@claudinho/mcp 0.8.15 → 0.8.17

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 +157 -39
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -54,7 +54,8 @@ Then just ask your agent naturally — it picks the right tool and answers with
54
54
 
55
55
  All tools are **read-only** (`readOnlyHint`) and accept optional `tz`, `lang`
56
56
  (`en`/`es`/`pt`/`fr`), and `flavor` (`off`/`subtle`/`full`). Every response carries
57
- human-readable text **and** structured JSON.
57
+ human-readable text **and** structured content, validated against each tool's
58
+ declared `outputSchema`.
58
59
 
59
60
  Resources: `standings://{group}`, `fixtures://{date}`. Prompts: `tournament_today`,
60
61
  and `my_team` (give it a 3-letter team code; combines next fixture, standings, and
package/dist/index.js CHANGED
@@ -3116,7 +3116,7 @@ function winnerLabel(ctx, stage, index) {
3116
3116
  n: String(index)
3117
3117
  });
3118
3118
  }
3119
- function resolveSlot(ref, ctx, liveTeam) {
3119
+ function resolveSlot(ref, ctx, liveTeam, fixtureInMergedSet = false) {
3120
3120
  const liveParticipant = confirmedLiveParticipant(liveTeam);
3121
3121
  switch (ref.kind) {
3122
3122
  case "seed":
@@ -3138,23 +3138,27 @@ function resolveSlot(ref, ctx, liveTeam) {
3138
3138
  if (liveParticipant) return liveParticipant;
3139
3139
  return tbd(t(ctx.lang, "bracket.slot.third", { groups: ref.groups.join("/") }));
3140
3140
  case "winner": {
3141
- const node = ctx.nodesByKey.get(matchKey(ref.stage, ref.index));
3142
- const match = node ? ctx.matchesById.get(node.matchId) : void 0;
3143
- if (match) {
3144
- const winner = resolveWinner(match);
3145
- if (winner) return participant(winner, "confirmed");
3146
- }
3147
3141
  if (liveParticipant) return liveParticipant;
3142
+ if (!fixtureInMergedSet) {
3143
+ const node = ctx.nodesByKey.get(matchKey(ref.stage, ref.index));
3144
+ const match = node ? ctx.matchesById.get(node.matchId) : void 0;
3145
+ if (match) {
3146
+ const winner = resolveWinner(match);
3147
+ if (winner) return participant(winner, "confirmed");
3148
+ }
3149
+ }
3148
3150
  return tbd(winnerLabel(ctx, ref.stage, ref.index));
3149
3151
  }
3150
3152
  case "loser": {
3151
- const node = ctx.nodesByKey.get(matchKey(ref.stage, ref.index));
3152
- const match = node ? ctx.matchesById.get(node.matchId) : void 0;
3153
- if (match) {
3154
- const loser = resolveLoser(match);
3155
- if (loser) return participant(loser, "confirmed");
3156
- }
3157
3153
  if (liveParticipant) return liveParticipant;
3154
+ if (!fixtureInMergedSet) {
3155
+ const node = ctx.nodesByKey.get(matchKey(ref.stage, ref.index));
3156
+ const match = node ? ctx.matchesById.get(node.matchId) : void 0;
3157
+ if (match) {
3158
+ const loser = resolveLoser(match);
3159
+ if (loser) return participant(loser, "confirmed");
3160
+ }
3161
+ }
3158
3162
  return tbd(
3159
3163
  t(ctx.lang, "bracket.slot.loser", {
3160
3164
  stage: stageLabelI18n(ctx.lang, ref.stage),
@@ -3177,6 +3181,7 @@ function buildBracketView(topology, matches, tables, standingsDegraded, liveDegr
3177
3181
  if (want && stage !== want) continue;
3178
3182
  const nodes = topology.matches.filter((n) => n.stage === stage);
3179
3183
  const matchViews = nodes.map((node) => {
3184
+ const fixtureInMergedSet = matchesById.has(node.matchId);
3180
3185
  const match = matchesById.get(node.matchId) ?? {
3181
3186
  id: node.matchId,
3182
3187
  stage: node.stage,
@@ -3192,8 +3197,8 @@ function buildBracketView(topology, matches, tables, standingsDegraded, liveDegr
3192
3197
  stage: node.stage,
3193
3198
  index: node.index,
3194
3199
  kickoff: match.kickoff,
3195
- home: resolveSlot(node.home, ctx, match.home),
3196
- away: resolveSlot(node.away, ctx, match.away),
3200
+ home: resolveSlot(node.home, ctx, match.home, fixtureInMergedSet),
3201
+ away: resolveSlot(node.away, ctx, match.away, fixtureInMergedSet),
3197
3202
  match
3198
3203
  };
3199
3204
  });
@@ -4035,8 +4040,8 @@ function marketProbabilityText(signal, match) {
4035
4040
  }
4036
4041
  function marketAttributionText(signal) {
4037
4042
  const time = utcHhmm(signal.asOf);
4038
- const src = `Source: ${marketSourceLabel(signal.source)}`;
4039
- return time ? `${src} \xB7 updated ${time}` : src;
4043
+ const src2 = `Source: ${marketSourceLabel(signal.source)}`;
4044
+ return time ? `${src2} \xB7 updated ${time}` : src2;
4040
4045
  }
4041
4046
  function marketLine(signal, match) {
4042
4047
  return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
@@ -4124,7 +4129,8 @@ var PolymarketProvider = class {
4124
4129
  opts;
4125
4130
  name = "polymarket";
4126
4131
  async findSignal(match, options) {
4127
- return (await this.resolveOne(match, options)).signal;
4132
+ const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4133
+ return (await this.resolveOne(match, options, deadline)).signal;
4128
4134
  }
4129
4135
  async findSignals(matches, options) {
4130
4136
  const signals = /* @__PURE__ */ new Map();
@@ -4132,7 +4138,7 @@ var PolymarketProvider = class {
4132
4138
  const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
4133
4139
  for (const m of matches) {
4134
4140
  if (Date.now() >= deadline) break;
4135
- const r = await this.resolveOne(m, options);
4141
+ const r = await this.resolveOne(m, options, deadline);
4136
4142
  if (r.checked) checked.add(m.id);
4137
4143
  if (r.signal) signals.set(m.id, r.signal);
4138
4144
  }
@@ -4144,13 +4150,16 @@ var PolymarketProvider = class {
4144
4150
  * provider/network error — so transient failures are retried, not
4145
4151
  * negative-cached.
4146
4152
  */
4147
- async resolveOne(match, options) {
4153
+ async resolveOne(match, options, deadline = Number.POSITIVE_INFINITY) {
4148
4154
  const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
4149
4155
  const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
4150
4156
  if (slugs.length === 0) return { checked: true };
4157
+ const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4151
4158
  try {
4152
4159
  for (const slug of slugs) {
4153
- const event = await this.fetchEvent(slug, options?.timeoutMs);
4160
+ const remaining = deadline - Date.now();
4161
+ if (remaining <= 0) return { checked: false };
4162
+ const event = await this.fetchEvent(slug, Math.min(configured, remaining));
4154
4163
  const signal = event ? this.toSignal(match, slug, event, options) : void 0;
4155
4164
  if (signal) return { signal, checked: true };
4156
4165
  }
@@ -4230,6 +4239,27 @@ var PolymarketProvider = class {
4230
4239
  return signal.ambiguous ? void 0 : signal;
4231
4240
  }
4232
4241
  };
4242
+ var POLYMARKET_TOKEN = {
4243
+ SUI: "che",
4244
+ // Switzerland
4245
+ NED: "nld",
4246
+ // Netherlands
4247
+ URU: "ury",
4248
+ // Uruguay
4249
+ POR: "prt",
4250
+ // Portugal
4251
+ CRO: "hrv",
4252
+ // Croatia
4253
+ COD: "cdr",
4254
+ // DR Congo
4255
+ CPV: "cvi"
4256
+ // Cabo Verde
4257
+ };
4258
+ function pmTokens(code) {
4259
+ const c = code.toLowerCase();
4260
+ const alias = POLYMARKET_TOKEN[code.toUpperCase()];
4261
+ return alias && alias !== c ? [alias, c] : [c];
4262
+ }
4233
4263
  function deriveEventSlugs(match) {
4234
4264
  const home = match.home.code.toLowerCase();
4235
4265
  const away = match.away.code.toLowerCase();
@@ -4237,10 +4267,18 @@ function deriveEventSlugs(match) {
4237
4267
  if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return [];
4238
4268
  const utcDate = match.kickoff.slice(0, 10);
4239
4269
  if (!/^\d{4}-\d{2}-\d{2}$/.test(utcDate)) return [];
4240
- return [
4241
- `fifwc-${home}-${away}-${utcDate}`,
4242
- `fifwc-${home}-${away}-${shiftUtcDate(utcDate, -1)}`
4243
- ];
4270
+ const dates = [utcDate, shiftUtcDate(utcDate, -1)];
4271
+ const homeToks = pmTokens(match.home.code);
4272
+ const awayToks = pmTokens(match.away.code);
4273
+ const slugs = [];
4274
+ for (const d of dates) {
4275
+ for (const h of homeToks) {
4276
+ for (const a of awayToks) {
4277
+ slugs.push(`fifwc-${h}-${a}-${d}`);
4278
+ }
4279
+ }
4280
+ }
4281
+ return [...new Set(slugs)];
4244
4282
  }
4245
4283
  function slugToken(m) {
4246
4284
  return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
@@ -4249,10 +4287,10 @@ function isDrawMarket(m) {
4249
4287
  return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
4250
4288
  }
4251
4289
  function pickMarket(markets, teamCode, teamName) {
4252
- const code = teamCode.toLowerCase();
4290
+ const tokens = pmTokens(teamCode);
4253
4291
  const name = teamName.trim().toLowerCase();
4254
4292
  const teamMarkets = markets.filter((m) => !isDrawMarket(m));
4255
- const bySlug = teamMarkets.find((m) => slugToken(m) === code);
4293
+ const bySlug = teamMarkets.find((m) => tokens.includes(slugToken(m)));
4256
4294
  if (bySlug) return bySlug;
4257
4295
  return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
4258
4296
  }
@@ -4534,8 +4572,8 @@ function formatShareBracket(input, options = {}) {
4534
4572
  }
4535
4573
  }
4536
4574
  const footer = [];
4537
- const src = input.source ?? input.view.source;
4538
- if (src) footer.push(t(locale, "live.data", { source: liveSourceLabel(src) }));
4575
+ const src2 = input.source ?? input.view.source;
4576
+ if (src2) footer.push(t(locale, "live.data", { source: liveSourceLabel(src2) }));
4539
4577
  footer.push(
4540
4578
  [includeHashtag ? SHARE_HASHTAG : "", SHARE_DISCLAIMER].filter(Boolean).join(" \xB7 ")
4541
4579
  );
@@ -5114,7 +5152,7 @@ async function toolGetShareSnippet(args) {
5114
5152
 
5115
5153
  // src/server.ts
5116
5154
  var SERVER_NAME = "claudinho";
5117
- var SERVER_VERSION = "0.8.15";
5155
+ var SERVER_VERSION = "0.8.17";
5118
5156
  var VOICE = asFlavorLevel(process.env.CLAUDINHO_FLAVOR) === "off" ? "" : `
5119
5157
  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.`;
5120
5158
  var INSTRUCTIONS = `Claudinho serves live scores, fixtures, and group standings for the 2026 men's football tournament.
@@ -5131,12 +5169,84 @@ var commonArgs = {
5131
5169
  lang: z.string().optional().describe("Locale for formatting: en, es, pt, fr (other locales fall back to en)"),
5132
5170
  flavor: flavorArg.optional().describe("Commentary flair: off, subtle, full (default: full)")
5133
5171
  };
5172
+ var teamRef = z.object({ code: z.string(), name: z.string(), flag: z.string() }).partial().passthrough();
5173
+ var scorePair = z.object({ home: z.number(), away: z.number() }).partial().passthrough();
5174
+ var matchOut = z.object({
5175
+ id: z.string(),
5176
+ stage: z.string().optional(),
5177
+ group: z.string().nullable().optional(),
5178
+ kickoff: z.string().optional(),
5179
+ venue: z.string().optional(),
5180
+ home: teamRef.optional(),
5181
+ away: teamRef.optional(),
5182
+ score: scorePair.nullable().optional(),
5183
+ shootout: scorePair.optional(),
5184
+ status: z.string().optional(),
5185
+ minute: z.number().nullable().optional(),
5186
+ winnerCode: z.string().optional()
5187
+ }).passthrough();
5188
+ var anyObj = z.object({}).passthrough();
5189
+ var src = z.string().nullable();
5190
+ var todayOut = {
5191
+ date: z.string(),
5192
+ degraded: z.boolean(),
5193
+ source: src,
5194
+ count: z.number(),
5195
+ matches: z.array(matchOut),
5196
+ marketSignals: z.record(anyObj).optional()
5197
+ };
5198
+ var liveOut = { degraded: z.boolean(), source: src, count: z.number(), matches: z.array(matchOut) };
5199
+ var matchDetailOut = {
5200
+ match: matchOut.nullable(),
5201
+ degraded: z.boolean().optional(),
5202
+ source: src.optional(),
5203
+ marketSignal: anyObj.nullable().optional()
5204
+ };
5205
+ var standingsOut = {
5206
+ degraded: z.boolean(),
5207
+ source: src,
5208
+ tables: z.union([anyObj, z.array(anyObj), z.null()])
5209
+ };
5210
+ var bracketOut = {
5211
+ view: anyObj.nullable(),
5212
+ degraded: z.boolean().optional(),
5213
+ standingsDegraded: z.boolean().optional(),
5214
+ source: src.optional()
5215
+ };
5216
+ var nextOut = { team: z.string(), fixture: matchOut.nullable(), degraded: z.boolean() };
5217
+ var marketOut = {
5218
+ matchId: z.string().nullable().optional(),
5219
+ team: z.string().optional(),
5220
+ date: z.string().optional(),
5221
+ degraded: z.boolean().optional(),
5222
+ informationalOnly: z.boolean(),
5223
+ signal: anyObj.nullable().optional(),
5224
+ signals: z.array(anyObj).optional()
5225
+ };
5226
+ var shareOut = {
5227
+ kind: z.string(),
5228
+ target: z.string().optional(),
5229
+ snippet: z.string().optional(),
5230
+ // absent on the bracket "unknown stage" error branch
5231
+ source: src.optional(),
5232
+ informationalOnly: z.boolean().optional(),
5233
+ degraded: z.boolean().optional(),
5234
+ style: z.string().optional(),
5235
+ team: z.string().optional(),
5236
+ group: z.string().optional(),
5237
+ stage: z.string().optional(),
5238
+ tables: z.union([anyObj, z.array(anyObj), z.null()]).optional(),
5239
+ view: anyObj.nullable().optional(),
5240
+ matches: z.array(matchOut).optional(),
5241
+ marketSignals: z.record(anyObj).optional()
5242
+ };
5134
5243
  function toContent(r) {
5135
5244
  return {
5136
5245
  content: [
5137
5246
  { type: "text", text: r.text },
5138
5247
  { type: "text", text: "```json\n" + JSON.stringify(r.data, null, 2) + "\n```" }
5139
- ]
5248
+ ],
5249
+ structuredContent: r.data
5140
5250
  };
5141
5251
  }
5142
5252
  function buildServer() {
@@ -5154,7 +5264,8 @@ function buildServer() {
5154
5264
  ...commonArgs
5155
5265
  },
5156
5266
  // Read-only; reaches an external data provider for the live overlay.
5157
- annotations: { readOnlyHint: true, openWorldHint: true }
5267
+ annotations: { readOnlyHint: true, openWorldHint: true },
5268
+ outputSchema: todayOut
5158
5269
  },
5159
5270
  async (args) => toContent(await toolGetToday(args))
5160
5271
  );
@@ -5164,7 +5275,8 @@ function buildServer() {
5164
5275
  title: "Live matches",
5165
5276
  description: "Only matches in play right now \u2014 each with current score and minute (empty when nothing is live). Use during matches for in-play state; for a full day's schedule including upcoming and finished, use get_today. tz/lang/flavor affect formatting only.",
5166
5277
  inputSchema: { ...commonArgs },
5167
- annotations: { readOnlyHint: true, openWorldHint: true }
5278
+ annotations: { readOnlyHint: true, openWorldHint: true },
5279
+ outputSchema: liveOut
5168
5280
  },
5169
5281
  async (args) => toContent(await toolGetLive(args))
5170
5282
  );
@@ -5174,7 +5286,8 @@ function buildServer() {
5174
5286
  title: "Match detail",
5175
5287
  description: "One match by its id, with live score/minute overlaid when it's in play. Get the id from get_today or get_live; to find a team's match without an id, use get_next_fixture. tz/lang/flavor affect formatting.",
5176
5288
  inputSchema: { id: z.string().describe("Match id"), ...commonArgs },
5177
- annotations: { readOnlyHint: true, openWorldHint: true }
5289
+ annotations: { readOnlyHint: true, openWorldHint: true },
5290
+ outputSchema: matchDetailOut
5178
5291
  },
5179
5292
  async (args) => toContent(await toolGetMatch(args))
5180
5293
  );
@@ -5187,7 +5300,8 @@ function buildServer() {
5187
5300
  group: groupArg.optional().describe("Group letter A\u2013L (omit for all)"),
5188
5301
  ...commonArgs
5189
5302
  },
5190
- annotations: { readOnlyHint: true, openWorldHint: true }
5303
+ annotations: { readOnlyHint: true, openWorldHint: true },
5304
+ outputSchema: standingsOut
5191
5305
  },
5192
5306
  async (args) => toContent(await toolGetStandings(args))
5193
5307
  );
@@ -5200,7 +5314,8 @@ function buildServer() {
5200
5314
  stage: z.enum(["R32", "R16", "QF", "SF", "3P", "F"]).optional().describe("Knockout round to show (omit for the full bracket)"),
5201
5315
  ...commonArgs
5202
5316
  },
5203
- annotations: { readOnlyHint: true, openWorldHint: true }
5317
+ annotations: { readOnlyHint: true, openWorldHint: true },
5318
+ outputSchema: bracketOut
5204
5319
  },
5205
5320
  async (args) => toContent(await toolGetBracket(args))
5206
5321
  );
@@ -5211,7 +5326,8 @@ function buildServer() {
5211
5326
  description: "A team's next match, live-resolved: a confirmed knockout tie (Round of 32 onward) is read from the live overlay, group fixtures from the bundled schedule. Use a 3-letter code, e.g. MEX, BRA, USA. Falls back to the bundled schedule if the provider is unreachable.",
5212
5327
  inputSchema: { team: teamArg.describe("3-letter team code, e.g. MEX"), ...commonArgs },
5213
5328
  // Read-only; overlays live provider data for knockout pairings, so open-world.
5214
- annotations: { readOnlyHint: true, openWorldHint: true }
5329
+ annotations: { readOnlyHint: true, openWorldHint: true },
5330
+ outputSchema: nextOut
5215
5331
  },
5216
5332
  async (args) => toContent(await toolGetNextFixture(args))
5217
5333
  );
@@ -5229,7 +5345,8 @@ function buildServer() {
5229
5345
  ...commonArgs
5230
5346
  },
5231
5347
  // Read-only; reaches an external prediction-market data provider.
5232
- annotations: { readOnlyHint: true, openWorldHint: true }
5348
+ annotations: { readOnlyHint: true, openWorldHint: true },
5349
+ outputSchema: marketOut
5233
5350
  },
5234
5351
  async (args) => toContent(await toolGetMarketSignal(args))
5235
5352
  );
@@ -5253,7 +5370,8 @@ function buildServer() {
5253
5370
  ...commonArgs
5254
5371
  },
5255
5372
  // Read-only; may reach the live/market data providers (live/today/match).
5256
- annotations: { readOnlyHint: true, openWorldHint: true }
5373
+ annotations: { readOnlyHint: true, openWorldHint: true },
5374
+ outputSchema: shareOut
5257
5375
  },
5258
5376
  async (args) => toContent(await toolGetShareSnippet(args))
5259
5377
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudinho/mcp",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
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,10 +57,11 @@
57
57
  "tsup": "^8.0.0",
58
58
  "typescript": "^5.7.0",
59
59
  "vitest": "^4.1.9",
60
- "@claudinho/core": "0.8.15"
60
+ "@claudinho/core": "0.8.17"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsup",
64
+ "build:mcpb": "pnpm run build && node scripts/build-mcpb.mjs",
64
65
  "dev": "tsup --watch",
65
66
  "test": "vitest run",
66
67
  "typecheck": "tsc --noEmit",