@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.
- package/README.md +4 -2
- package/dist/index.js +134 -12
- 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
|
-
|
|
56
|
-
(`en`/`es`/`pt`/`fr`), and `flavor` (`off`/`subtle`/`full`)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
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
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
60
|
+
"@claudinho/core": "0.8.18"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|