@claudinho/mcp 0.8.16 → 0.8.18

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 +4 -2
  2. package/dist/index.js +134 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -51,9 +51,11 @@ Then just ask your agent naturally — it picks the right tool and answers with
51
51
  | `get_next_fixture` | a team's next match (3-letter code, e.g. `MEX`) — live-resolves a confirmed knockout tie from the feed; group fixtures offline, fails back to the bundled schedule if the feed is down |
52
52
  | `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 |
53
53
  | `get_share_snippet` | a copy-pasteable plain-text card — for a match, a team's next fixture, a group's standings table (`group`), the knockout bracket (`bracket: true`, optional `knockoutStage`), a date, or live — hand the returned snippet to the user as-is |
54
+ | `get_team` | resolve a nation name or code to its FIFA 3-letter code, flag, and group — fuzzy (`Mexico`, `mex`, `DR Congo`, `Türkiye`); call it first to turn a user's team name into the code the other tools need. Offline (no network) |
54
55
 
55
- All tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
56
- (`en`/`es`/`pt`/`fr`), and `flavor` (`off`/`subtle`/`full`). Every response carries
56
+ Most tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
57
+ (`en`/`es`/`pt`/`fr`), and `flavor` (`off`/`subtle`/`full`); `get_team` is read-only
58
+ **and** offline. Every response carries
57
59
  human-readable text **and** structured content, validated against each tool's
58
60
  declared `outputSchema`.
59
61
 
package/dist/index.js CHANGED
@@ -2802,6 +2802,60 @@ function groups(fixtures = SCHEDULE) {
2802
2802
  for (const m of fixtures) if (m.group) set.add(m.group);
2803
2803
  return [...set].sort();
2804
2804
  }
2805
+ function norm2(s) {
2806
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z]/g, "");
2807
+ }
2808
+ function allTeams(fixtures = allFixtures()) {
2809
+ const seen = /* @__PURE__ */ new Map();
2810
+ for (const m of fixtures) {
2811
+ if (m.stage !== "GROUP") continue;
2812
+ for (const t2 of [m.home, m.away]) {
2813
+ if (!/^[A-Z]{3}$/.test(t2.code) || t2.flag === "\u{1F3F3}\uFE0F") continue;
2814
+ if (!seen.has(t2.code)) seen.set(t2.code, { ...t2, group: m.group ?? void 0 });
2815
+ }
2816
+ }
2817
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
2818
+ }
2819
+ var TEAM_ALIASES = {
2820
+ turkey: "TUR",
2821
+ holland: "NED",
2822
+ korea: "KOR",
2823
+ skorea: "KOR",
2824
+ korearepublic: "KOR",
2825
+ republicofkorea: "KOR",
2826
+ czechrepublic: "CZE",
2827
+ congo: "COD",
2828
+ drcongo: "COD",
2829
+ drc: "COD",
2830
+ democraticrepublicofcongo: "COD",
2831
+ democraticrepublicofthecongo: "COD",
2832
+ cotedivoire: "CIV",
2833
+ caboverde: "CPV",
2834
+ bosnia: "BIH",
2835
+ bosniaandherzegovina: "BIH",
2836
+ us: "USA",
2837
+ america: "USA",
2838
+ unitedstatesofamerica: "USA"
2839
+ };
2840
+ function lookupTeam(query, fixtures = allFixtures()) {
2841
+ const roster = allTeams(fixtures);
2842
+ const q = norm2(query);
2843
+ if (!q) return { query, team: null, matches: [] };
2844
+ const byCode = roster.find((t2) => norm2(t2.code) === q);
2845
+ if (byCode) return { query, team: byCode, matches: [byCode] };
2846
+ const byName = roster.find((t2) => norm2(t2.name) === q);
2847
+ if (byName) return { query, team: byName, matches: [byName] };
2848
+ const aliasCode = TEAM_ALIASES[q];
2849
+ if (aliasCode) {
2850
+ const t2 = roster.find((r) => r.code === aliasCode);
2851
+ if (t2) return { query, team: t2, matches: [t2] };
2852
+ }
2853
+ if (q.length < 3) return { query, team: null, matches: [] };
2854
+ const prefix = roster.filter((t2) => norm2(t2.name).startsWith(q));
2855
+ const substr = roster.filter((t2) => norm2(t2.name).includes(q) && !prefix.includes(t2));
2856
+ const matches = [...prefix, ...substr];
2857
+ return { query, team: matches.length === 1 ? matches[0] : null, matches };
2858
+ }
2805
2859
  function blankRow(team) {
2806
2860
  return {
2807
2861
  team,
@@ -4129,7 +4183,8 @@ var PolymarketProvider = class {
4129
4183
  opts;
4130
4184
  name = "polymarket";
4131
4185
  async findSignal(match, options) {
4132
- return (await this.resolveOne(match, options)).signal;
4186
+ const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4187
+ return (await this.resolveOne(match, options, deadline)).signal;
4133
4188
  }
4134
4189
  async findSignals(matches, options) {
4135
4190
  const signals = /* @__PURE__ */ new Map();
@@ -4137,7 +4192,7 @@ var PolymarketProvider = class {
4137
4192
  const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4138
4193
  for (const m of matches) {
4139
4194
  if (Date.now() >= deadline) break;
4140
- const r = await this.resolveOne(m, options);
4195
+ const r = await this.resolveOne(m, options, deadline);
4141
4196
  if (r.checked) checked.add(m.id);
4142
4197
  if (r.signal) signals.set(m.id, r.signal);
4143
4198
  }
@@ -4149,13 +4204,16 @@ var PolymarketProvider = class {
4149
4204
  * provider/network error — so transient failures are retried, not
4150
4205
  * negative-cached.
4151
4206
  */
4152
- async resolveOne(match, options) {
4207
+ async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4153
4208
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4154
4209
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4155
4210
  if (slugs.length === 0) return { checked: true };
4211
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4156
4212
  try {
4157
4213
  for (const slug of slugs) {
4158
- const event = await this.fetchEvent(slug, options?.timeoutMs);
4214
+ const remaining = deadline - Date.now();
4215
+ if (remaining <= 0) return { checked: false };
4216
+ const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4159
4217
  const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4160
4218
  if (signal) return { signal, checked: true };
4161
4219
  }
@@ -4235,6 +4293,27 @@ var PolymarketProvider = class {
4235
4293
  return signal.ambiguous ? void 0 : signal;
4236
4294
  }
4237
4295
  };
4296
+ var POLYMARKET_TOKEN = {
4297
+ SUI: "che",
4298
+ // Switzerland
4299
+ NED: "nld",
4300
+ // Netherlands
4301
+ URU: "ury",
4302
+ // Uruguay
4303
+ POR: "prt",
4304
+ // Portugal
4305
+ CRO: "hrv",
4306
+ // Croatia
4307
+ COD: "cdr",
4308
+ // DR Congo
4309
+ CPV: "cvi"
4310
+ // Cabo Verde
4311
+ };
4312
+ function pmTokens(code) {
4313
+ const c = code.toLowerCase();
4314
+ const alias = POLYMARKET_TOKEN[code.toUpperCase()];
4315
+ return alias && alias !== c ? [alias, c] : [c];
4316
+ }
4238
4317
  function deriveEventSlugs(match) {
4239
4318
  const home = match.home.code.toLowerCase();
4240
4319
  const away = match.away.code.toLowerCase();
@@ -4242,10 +4321,18 @@ function deriveEventSlugs(match) {
4242
4321
  if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return [];
4243
4322
  const utcDate = match.kickoff.slice(0, 10);
4244
4323
  if (!/^\d{4}-\d{2}-\d{2}$/.test(utcDate)) return [];
4245
- return [
4246
- `fifwc-${home}-${away}-${utcDate}`,
4247
- `fifwc-${home}-${away}-${shiftUtcDate(utcDate, -1)}`
4248
- ];
4324
+ const dates = [utcDate, shiftUtcDate(utcDate, -1)];
4325
+ const homeToks = pmTokens(match.home.code);
4326
+ const awayToks = pmTokens(match.away.code);
4327
+ const slugs = [];
4328
+ for (const d of dates) {
4329
+ for (const h of homeToks) {
4330
+ for (const a of awayToks) {
4331
+ slugs.push(`fifwc-${h}-${a}-${d}`);
4332
+ }
4333
+ }
4334
+ }
4335
+ return [...new Set(slugs)];
4249
4336
  }
4250
4337
  function slugToken(m) {
4251
4338
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
@@ -4254,10 +4341,10 @@ function isDrawMarket(m) {
4254
4341
  return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
4255
4342
  }
4256
4343
  function pickMarket(markets, teamCode, teamName) {
4257
- const code = teamCode.toLowerCase();
4344
+ const tokens = pmTokens(teamCode);
4258
4345
  const name = teamName.trim().toLowerCase();
4259
4346
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4260
- const bySlug = teamMarkets.find((m) => slugToken(m) === code);
4347
+ const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4261
4348
  if (bySlug) return bySlug;
4262
4349
  return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
4263
4350
  }
@@ -4851,6 +4938,19 @@ ${matchLine(fixture, opts)}`, source, args.lang),
4851
4938
  data: { team: code, fixture, degraded }
4852
4939
  };
4853
4940
  }
4941
+ function toolGetTeam(args) {
4942
+ const { team, matches } = lookupTeam(args.query ?? "");
4943
+ const data = { query: args.query ?? "", team: team ?? null, matches, count: matches.length };
4944
+ let text;
4945
+ if (team) {
4946
+ text = `${team.code} \u2014 ${team.flag} ${team.name}${team.group ? ` \xB7 Group ${team.group}` : ""}`;
4947
+ } else if (matches.length > 0) {
4948
+ text = `"${args.query}" is ambiguous. Did you mean: ${matches.map((t2) => `${t2.name} (${t2.code})`).join(", ")}?`;
4949
+ } else {
4950
+ text = `No team found for "${args.query}". Use a nation name or 3-letter code (e.g. Mexico, MEX).`;
4951
+ }
4952
+ return { text: withDisclaimer(text), data };
4953
+ }
4854
4954
  async function toolGetMarketSignal(args) {
4855
4955
  const provider = resolveMarketProvider(args);
4856
4956
  const now = args.now ?? /* @__PURE__ */ new Date();
@@ -5119,11 +5219,12 @@ async function toolGetShareSnippet(args) {
5119
5219
 
5120
5220
  // src/server.ts
5121
5221
  var SERVER_NAME = "claudinho";
5122
- var SERVER_VERSION = "0.8.16";
5222
+ var SERVER_VERSION = "0.8.18";
5123
5223
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5124
5224
  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.`;
5125
5225
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
5126
- 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), get_standings for group tables, and get_bracket for the knockout tree.
5226
+ The team-taking tools (get_next_fixture, get_market_signal, get_share_snippet) expect a 3-letter code (e.g. MEX). When the user gives a nation NAME, call get_team FIRST to resolve it \u2014 get_team is fuzzy ("Mexico", "DR Congo", "T\xFCrkiye"), offline, and returns candidates when the name is ambiguous.
5227
+ Use get_live during matches, get_today for a day's schedule, get_next_fixture for a specific team, get_standings for group tables, and get_bracket for the knockout tree.
5127
5228
  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.
5128
5229
  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}
5129
5230
  ${DISCLAIMER}`;
@@ -5207,6 +5308,13 @@ var shareOut = {
5207
5308
  matches: z.array(matchOut).optional(),
5208
5309
  marketSignals: z.record(anyObj).optional()
5209
5310
  };
5311
+ var teamInfo = z.object({ code: z.string(), name: z.string(), flag: z.string(), group: z.string() }).partial().passthrough();
5312
+ var teamOut = {
5313
+ query: z.string(),
5314
+ team: teamInfo.nullable(),
5315
+ matches: z.array(teamInfo),
5316
+ count: z.number()
5317
+ };
5210
5318
  function toContent(r) {
5211
5319
  return {
5212
5320
  content: [
@@ -5342,6 +5450,20 @@ function buildServer() {
5342
5450
  },
5343
5451
  async (args) => toContent(await toolGetShareSnippet(args))
5344
5452
  );
5453
+ server.registerTool(
5454
+ "get_team",
5455
+ {
5456
+ title: "Resolve a team",
5457
+ description: `Resolve a nation name or 3-letter code to its FIFA code, flag, and group. Fuzzy and forgiving: accepts "Mexico", "mex", "USA", "DR Congo", "T\xFCrkiye"/"Turkey", "Holland", etc. Use this FIRST to turn a user's team name into the code the other tools need (get_next_fixture, get_standings, get_market_signal, get_share_snippet). Returns the single confident match (team), plus candidates (matches) when the query is ambiguous (e.g. "south" \u2192 South Africa, South Korea). Offline \u2014 reads the bundled roster, never the network.`,
5458
+ inputSchema: {
5459
+ query: z.string().describe('Team name or 3-letter code, e.g. "Mexico", "MEX", "DR Congo"')
5460
+ },
5461
+ // Read-only AND offline — resolves against the bundled roster, no provider call.
5462
+ annotations: { readOnlyHint: true, openWorldHint: false },
5463
+ outputSchema: teamOut
5464
+ },
5465
+ async (args) => toContent(toolGetTeam(args))
5466
+ );
5345
5467
  server.registerResource(
5346
5468
  "standings",
5347
5469
  new ResourceTemplate("standings://{group}", { list: void 0 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
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",
@@ -57,7 +57,7 @@
57
57
  "tsup": "^8.0.0",
58
58
  "typescript": "^5.7.0",
59
59
  "vitest": "^4.1.9",
60
- "@claudinho/core": "0.8.16"
60
+ "@claudinho/core": "0.8.18"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",