@claudinho/mcp 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +272 -4
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -49,6 +49,7 @@ any other MCP client with no changes.
49
49
  | `get_standings` | group table(s) — one group `A`–`L`, or all |
50
50
  | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) |
51
51
  | `get_market_signal` | read-only prediction-market odds for a match, a team's next fixture, or a date — informational only |
52
+ | `get_share_snippet` | a copy-pasteable match-card snippet for a match, a team's next fixture, a date, or live — plain text, ready to paste |
52
53
 
53
54
  All tools are **read-only** (annotated `readOnlyHint`) and accept optional
54
55
  `tz` (IANA timezone), `lang` (`en`/`es`/`pt`/`fr`), and `flavor`
@@ -64,6 +65,12 @@ cleanly to the result. Disable it with `CLAUDINHO_MARKETS=off`; set
64
65
  `CLAUDINHO_MARKETS_SOURCE=fake` (in the server `env`) for a network-free synthetic
65
66
  preview.
66
67
 
68
+ `get_share_snippet` returns a polished, **copy-pasteable** match card (the same
69
+ artifact as the CLI's `claudinho share`) for a match, a team's next fixture, a
70
+ date, or live matches — plain text, no links, with the non-affiliation disclaimer
71
+ baked in. Hand the returned `snippet` to the user as-is. `style` picks `social`
72
+ (default) or `compact`; market lines (when reliable) stay **informational only**.
73
+
67
74
  ## Resources & prompts
68
75
 
69
76
  - Resources: `standings://{group}` (e.g. `standings://A`), `fixtures://{date}` (UTC date, e.g. `fixtures://2026-06-11`)
package/dist/index.js CHANGED
@@ -315,6 +315,25 @@ function formatKickoff(iso, opts = {}) {
315
315
  timeZone: tz
316
316
  }).format(new Date(iso));
317
317
  }
318
+ function formatDate(iso, opts = {}) {
319
+ const tz = resolveTz(opts.tz);
320
+ const locale = safeLocale(opts.locale);
321
+ return new Intl.DateTimeFormat(locale, {
322
+ month: "short",
323
+ day: "numeric",
324
+ timeZone: tz
325
+ }).format(new Date(iso));
326
+ }
327
+ function formatTime(iso, opts = {}) {
328
+ const tz = resolveTz(opts.tz);
329
+ const locale = safeLocale(opts.locale);
330
+ return new Intl.DateTimeFormat(locale, {
331
+ hour: "2-digit",
332
+ minute: "2-digit",
333
+ hour12: false,
334
+ timeZone: tz
335
+ }).format(new Date(iso));
336
+ }
318
337
  function countdown(iso, from = /* @__PURE__ */ new Date()) {
319
338
  const ms = new Date(iso).getTime() - from.getTime();
320
339
  if (ms <= 0) return "now";
@@ -335,6 +354,9 @@ function localDate(iso, tz) {
335
354
  timeZone: zone
336
355
  }).format(new Date(iso));
337
356
  }
357
+ function isLive(status) {
358
+ return status === "LIVE" || status === "HT";
359
+ }
338
360
  function isFinished(status) {
339
361
  return status === "FT";
340
362
  }
@@ -2989,6 +3011,11 @@ function marketAttributionText(signal) {
2989
3011
  const src = `Source: ${marketSourceLabel(signal.source)}`;
2990
3012
  return time ? `${src} \xB7 updated ${time}` : src;
2991
3013
  }
3014
+ function marketLine(signal, match) {
3015
+ return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
3016
+ signal.source
3017
+ )} \xB7 informational only`;
3018
+ }
2992
3019
  function marketBlock(signal, match) {
2993
3020
  const lines = [];
2994
3021
  if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
@@ -3272,6 +3299,87 @@ async function getMarketSignals(provider, matches, options) {
3272
3299
  return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
3273
3300
  }
3274
3301
  }
3302
+ var SHARE_HASHTAG = "#VibingLaVidaLoca";
3303
+ var SHARE_DISCLAIMER = "Independent fan project \xB7 not affiliated with FIFA or Anthropic.";
3304
+ function mid(m) {
3305
+ return isLive(m.status) || m.status === "FT" ? scoreline(m) : "vs";
3306
+ }
3307
+ function statusTail(m) {
3308
+ switch (m.status) {
3309
+ case "LIVE":
3310
+ return m.minute ? `${m.minute}'` : "LIVE";
3311
+ case "HT":
3312
+ return "HT";
3313
+ case "FT":
3314
+ return "FT";
3315
+ case "POSTPONED":
3316
+ return "postponed";
3317
+ case "CANCELLED":
3318
+ return "cancelled";
3319
+ default:
3320
+ return "";
3321
+ }
3322
+ }
3323
+ function compactLine(m, input, single) {
3324
+ const home = `${m.home.flag} ${m.home.code}`;
3325
+ const away = `${m.away.code} ${m.away.flag}`;
3326
+ const opts = { tz: input.tz, locale: input.locale };
3327
+ let tail;
3328
+ if (m.status === "SCHEDULED") {
3329
+ const time = formatTime(m.kickoff, opts);
3330
+ tail = single ? `${formatDate(m.kickoff, opts)} ${time}` : time;
3331
+ } else {
3332
+ tail = statusTail(m);
3333
+ }
3334
+ return `${home} ${mid(m)} ${away}${tail ? ` \xB7 ${tail}` : ""}`;
3335
+ }
3336
+ function socialCard(m, input) {
3337
+ const lines = [];
3338
+ const head = `${m.home.flag} ${m.home.name} ${mid(m)} ${m.away.name} ${m.away.flag}`;
3339
+ if (m.status === "SCHEDULED") {
3340
+ lines.push(head);
3341
+ const date = formatDate(m.kickoff, { tz: input.tz, locale: input.locale });
3342
+ const time = formatTime(m.kickoff, { tz: input.tz, locale: input.locale });
3343
+ const zone = input.tz ? ` ${input.tz}` : "";
3344
+ lines.push(`${date} \xB7 ${time}${zone}`);
3345
+ } else {
3346
+ const tail = statusTail(m);
3347
+ lines.push(tail ? `${head} \xB7 ${tail}` : head);
3348
+ }
3349
+ const loc = matchLocation(m);
3350
+ if (loc) lines.push(loc);
3351
+ return lines;
3352
+ }
3353
+ function formatShareSnippet(input, options = {}) {
3354
+ const style = options.style ?? "social";
3355
+ const includeMarkets = options.includeMarkets !== false;
3356
+ const includeHashtag = options.includeHashtag !== false;
3357
+ const includeInstall = options.includeInstallLine !== false;
3358
+ const signals = input.marketSignals ?? /* @__PURE__ */ new Map();
3359
+ const single = input.matches.length === 1;
3360
+ const blocks = [input.title];
3361
+ if (input.matches.length === 0) {
3362
+ if (input.emptyNote) blocks.push(input.emptyNote);
3363
+ } else if (style === "compact") {
3364
+ blocks.push(input.matches.map((m) => compactLine(m, input, single)).join("\n"));
3365
+ } else {
3366
+ for (const m of input.matches) {
3367
+ const card = socialCard(m, input);
3368
+ const sig = includeMarkets ? signals.get(m.id) : void 0;
3369
+ if (sig) {
3370
+ if (single) card.push("", ...marketBlock(sig, m));
3371
+ else card.push(marketLine(sig, m));
3372
+ }
3373
+ blocks.push(card.join("\n"));
3374
+ }
3375
+ }
3376
+ const footer = [];
3377
+ if (input.source) footer.push(`Live data: ${liveSourceLabel(input.source)}`);
3378
+ footer.push([includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 "));
3379
+ if (includeInstall && input.installLine) footer.push(`Try it: ${input.installLine}`);
3380
+ blocks.push(footer.join("\n"));
3381
+ return blocks.join("\n\n");
3382
+ }
3275
3383
 
3276
3384
  // src/format.ts
3277
3385
  var STATUS_LABEL = {
@@ -3596,15 +3704,154 @@ ${shown.map(({ match, signal }) => marketText(match, signal)).join("\n\n")}` : `
3596
3704
  }
3597
3705
  };
3598
3706
  }
3707
+ async function reliableSignalMap(args, matches) {
3708
+ if (!marketsEnabled()) return /* @__PURE__ */ new Map();
3709
+ const signals = await cachedMarketSignals(args, matches);
3710
+ const now = /* @__PURE__ */ new Date();
3711
+ const out = /* @__PURE__ */ new Map();
3712
+ for (const m of matches) {
3713
+ const s = signals.get(m.id);
3714
+ if (s && isReliableMarketSignal(s, { now }) && marketDisplayable(s)) out.set(m.id, s);
3715
+ }
3716
+ return out;
3717
+ }
3718
+ function shareOptions(args) {
3719
+ return {
3720
+ style: args.style === "compact" ? "compact" : "social",
3721
+ includeMarkets: marketsEnabled() && args.includeMarkets !== false,
3722
+ includeHashtag: args.includeHashtag !== false,
3723
+ includeInstallLine: args.includeInstallLine !== false
3724
+ };
3725
+ }
3726
+ function shareResult(kind, target, team, input, options) {
3727
+ const snippet = formatShareSnippet(input, options);
3728
+ return {
3729
+ // The snippet is self-contained: it carries its own non-affiliation
3730
+ // disclaimer (and, for any market line, the "informational only" caveat +
3731
+ // attribution), so it is deliberately NOT wrapped with withDisclaimer —
3732
+ // that would duplicate the disclaimer inside a paste-ready artifact.
3733
+ text: snippet,
3734
+ data: {
3735
+ kind,
3736
+ target,
3737
+ ...team ? { team } : {},
3738
+ source: input.source ?? null,
3739
+ informationalOnly: true,
3740
+ style: options.style ?? "social",
3741
+ snippet,
3742
+ matches: input.matches,
3743
+ marketSignals: Object.fromEntries(
3744
+ [...input.marketSignals ?? /* @__PURE__ */ new Map()].map(([id, s]) => [
3745
+ id,
3746
+ marketData(s)
3747
+ ])
3748
+ )
3749
+ }
3750
+ };
3751
+ }
3752
+ async function toolGetShareSnippet(args) {
3753
+ const options = shareOptions(args);
3754
+ const signalsFor = (ms) => args.includeMarkets === false ? Promise.resolve(/* @__PURE__ */ new Map()) : reliableSignalMap(args, ms);
3755
+ if (args.live) {
3756
+ const { matches, source: source2 } = await getLiveMatches(resolveAdapter(args));
3757
+ return shareResult(
3758
+ "live",
3759
+ "live",
3760
+ void 0,
3761
+ {
3762
+ title: "Live match pulse",
3763
+ matches,
3764
+ source: source2,
3765
+ emptyNote: "No matches in play right now.",
3766
+ installLine: "npx @claudinho/cli live",
3767
+ tz: args.tz,
3768
+ locale: args.lang
3769
+ },
3770
+ { ...options, includeMarkets: false }
3771
+ );
3772
+ }
3773
+ 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
+ }
3785
+ const matches = match ? [match] : [];
3786
+ return shareResult(
3787
+ "match",
3788
+ args.matchId,
3789
+ void 0,
3790
+ {
3791
+ title: "Match pulse",
3792
+ matches,
3793
+ marketSignals: await signalsFor(matches),
3794
+ source: source2,
3795
+ emptyNote: `No match found with id ${args.matchId}.`,
3796
+ installLine: `npx @claudinho/cli match ${args.matchId}`,
3797
+ tz: args.tz,
3798
+ locale: args.lang
3799
+ },
3800
+ options
3801
+ );
3802
+ }
3803
+ if (args.team) {
3804
+ const code = args.team.toUpperCase();
3805
+ const fixture = nextFixtureForTeam(code);
3806
+ const matches = fixture ? [fixture] : [];
3807
+ const teamName = fixture ? fixture.home.code === code ? fixture.home.name : fixture.away.name : code;
3808
+ return shareResult(
3809
+ "next",
3810
+ "next",
3811
+ code,
3812
+ {
3813
+ title: `Next up for ${teamName}`,
3814
+ matches,
3815
+ marketSignals: await signalsFor(matches),
3816
+ emptyNote: `No upcoming fixture found for ${code}.`,
3817
+ installLine: `npx @claudinho/cli next ${code}`,
3818
+ tz: args.tz,
3819
+ locale: args.lang
3820
+ },
3821
+ options
3822
+ );
3823
+ }
3824
+ const date = args.date ?? localDate((/* @__PURE__ */ new Date()).toISOString(), args.tz);
3825
+ const { matches: all, source } = await getMatchesForDate(resolveAdapter(args), date);
3826
+ const todays = fixturesByDate(date, all, args.tz);
3827
+ const human = formatDate(`${date}T12:00:00.000Z`, { tz: args.tz, locale: args.lang });
3828
+ return shareResult(
3829
+ "today",
3830
+ date,
3831
+ void 0,
3832
+ {
3833
+ title: args.date ? `Matches \xB7 ${human}` : `Today's matches \xB7 ${human}`,
3834
+ matches: todays,
3835
+ marketSignals: await signalsFor(todays),
3836
+ source,
3837
+ emptyNote: `No matches scheduled for ${human}.`,
3838
+ installLine: "npx @claudinho/cli today",
3839
+ tz: args.tz,
3840
+ locale: args.lang
3841
+ },
3842
+ options
3843
+ );
3844
+ }
3599
3845
 
3600
3846
  // src/server.ts
3601
3847
  var SERVER_NAME = "claudinho";
3602
- var SERVER_VERSION = "0.3.0";
3848
+ var SERVER_VERSION = "0.4.1";
3603
3849
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
3604
3850
  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.`;
3605
3851
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
3606
3852
  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.
3607
- 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.${VOICE}
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.
3854
+ 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}
3608
3855
  ${DISCLAIMER}`;
3609
3856
  var dateArg = z.string().refine(isValidDate, "must be a real calendar date in YYYY-MM-DD form");
3610
3857
  var groupArg = z.string().regex(/^[A-La-l]$/, "a group letter A\u2013L");
@@ -3702,6 +3949,27 @@ function buildServer() {
3702
3949
  },
3703
3950
  async (args) => toContent(await toolGetMarketSignal(args))
3704
3951
  );
3952
+ server.registerTool(
3953
+ "get_share_snippet",
3954
+ {
3955
+ title: "Shareable match snippet",
3956
+ description: "A polished, copy-pasteable match card (plain text) for a match (matchId), a team's next fixture (team), a date (default: today), or live matches (live: true). Returns the ready-to-paste snippet plus structured data \u2014 hand the snippet text to the user verbatim. No links; it carries a non-affiliation disclaimer, and any market line stays informational only.",
3957
+ inputSchema: {
3958
+ matchId: z.string().optional().describe("Match id (most specific)"),
3959
+ team: teamArg.optional().describe("3-letter team code for that team's next fixture, e.g. MEX"),
3960
+ date: dateArg.optional().describe("Date as YYYY-MM-DD (default: today)"),
3961
+ live: z.boolean().optional().describe("Snapshot of matches in play right now"),
3962
+ style: z.enum(["social", "compact"]).optional().describe("social (default, full card) or compact (one line per match)"),
3963
+ includeHashtag: z.boolean().optional().describe("Include the #VibingLaVidaLoca tag (default true)"),
3964
+ includeInstallLine: z.boolean().optional().describe('Include the "Try it: \u2026" run cue (default true)'),
3965
+ includeMarkets: z.boolean().optional().describe("Include the reliable market line when available (default true)"),
3966
+ ...commonArgs
3967
+ },
3968
+ // Read-only; may reach the live/market data providers (live/today/match).
3969
+ annotations: { readOnlyHint: true, openWorldHint: true }
3970
+ },
3971
+ async (args) => toContent(await toolGetShareSnippet(args))
3972
+ );
3705
3973
  server.registerResource(
3706
3974
  "standings",
3707
3975
  new ResourceTemplate("standings://{group}", { list: void 0 }),
@@ -3755,7 +4023,7 @@ function buildServer() {
3755
4023
  "my_team",
3756
4024
  {
3757
4025
  title: "My team",
3758
- description: "Focus on one team's next match and group situation.",
4026
+ description: "Focus on one team's next match, group situation, and the prediction-market read.",
3759
4027
  argsSchema: { team: teamArg.describe("3-letter team code, e.g. MEX") }
3760
4028
  },
3761
4029
  ({ team }) => ({
@@ -3764,7 +4032,7 @@ function buildServer() {
3764
4032
  role: "user",
3765
4033
  content: {
3766
4034
  type: "text",
3767
- text: `Using get_next_fixture and get_standings, tell me about ${team}'s next match in the 2026 tournament and their current group standing.`
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.`
3768
4036
  }
3769
4037
  }
3770
4038
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Claudinho MCP server — ask your agent about the 2026 men's football tournament: live scores, fixtures, standings, prediction-market odds. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "tsup": "^8.0.0",
48
48
  "typescript": "^5.7.0",
49
49
  "vitest": "^4.1.0",
50
- "@claudinho/core": "0.3.0"
50
+ "@claudinho/core": "0.4.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup",