football-docs 0.2.0 → 0.3.0

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 CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  Searchable football data provider documentation for AI coding agents. Like [Context7](https://context7.com) for football data.
4
4
 
5
- An MCP server that gives your AI agent instant access to documentation for Opta, StatsBomb, Wyscout, SportMonks, socceraction, kloppy, soccerdata, mplsoccer, databallpy, and free sources (FBref, Understat, ClubElo). Search event types, qualifier IDs, coordinate systems, API endpoints, and cross-provider mappings.
5
+ **Who it's for:** Developers and analysts who use AI coding tools (Claude Code, Cursor, VS Code Copilot, Windsurf) to work with football data. Works with any tool that supports MCP.
6
+
7
+ **What it does:** Gives your AI agent a searchable index of documentation for 16 football data providers — event types, qualifier IDs, coordinate systems, API endpoints, data models, and cross-provider comparisons. Your agent looks up the real docs instead of guessing from training data.
8
+
9
+ **Why not just let the AI figure it out?** LLMs get football data specifics wrong constantly — Opta qualifier IDs, StatsBomb coordinate ranges, API endpoint URLs, library method signatures. These are mutable facts that change across versions. football-docs gives the agent verified, sourced documentation with provenance tracking so you know where every answer came from.
6
10
 
7
11
  MCP ([Model Context Protocol](https://modelcontextprotocol.io)) is a standard for connecting AI coding tools to external data sources.
8
12
 
package/data/docs.db CHANGED
Binary file
package/dist/index.js CHANGED
@@ -79,7 +79,7 @@ server.tool("search_docs", "Search football data provider documentation. Use for
79
79
  .optional()
80
80
  .default(10)
81
81
  .describe("Maximum number of results to return (default 10)"),
82
- }, async ({ query, provider, max_results }) => {
82
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ query, provider, max_results }) => {
83
83
  const db = openDb();
84
84
  try {
85
85
  const safeQuery = sanitiseFtsQuery(query);
@@ -131,7 +131,7 @@ server.tool("search_docs", "Search football data provider documentation. Use for
131
131
  }
132
132
  });
133
133
  // Tool: list_providers
134
- server.tool("list_providers", "List all indexed football data providers, their document count, and coverage categories. Use to understand what documentation is available.", {}, async () => {
134
+ server.tool("list_providers", "List all indexed football data providers, their document count, and coverage categories. Use to understand what documentation is available. Call this first to see what providers are indexed before searching.", {}, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async () => {
135
135
  const db = openDb();
136
136
  try {
137
137
  const rows = db
@@ -167,7 +167,7 @@ server.tool("list_providers", "List all indexed football data providers, their d
167
167
  server.tool("compare_providers", "Compare what two or more providers offer for a specific data type or concept. For example: 'How do Opta and StatsBomb represent shot events differently?'", {
168
168
  topic: z.string().describe("The concept to compare across providers. Examples: 'shot events', 'coordinate systems', 'xG', 'pass types'"),
169
169
  providers: z.array(z.string()).optional().describe("Providers to compare. If omitted, compares all indexed providers."),
170
- }, async ({ topic, providers }) => {
170
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ topic, providers }) => {
171
171
  const db = openDb();
172
172
  try {
173
173
  const safeTopic = sanitiseFtsQuery(topic);
@@ -225,7 +225,7 @@ server.tool("request_update", "Request that a provider's documentation be added,
225
225
  provider: z.string().max(100).describe("Provider name (existing or proposed). Examples: 'statsbomb', 'mplsoccer', 'floodlight'"),
226
226
  reason: z.string().max(2000).describe("Why this update is needed. Be specific: version bump, missing event types, new API endpoints, etc."),
227
227
  suggested_urls: z.array(z.string().url().max(500)).max(10).optional().describe("URLs for documentation sources (readthedocs, GitHub, llms.txt, etc.)"),
228
- }, async ({ type, provider, reason, suggested_urls }) => {
228
+ }, { readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ type, provider, reason, suggested_urls }) => {
229
229
  const qdb = openQueueDb();
230
230
  try {
231
231
  // Hard cap on queue size
@@ -279,6 +279,79 @@ server.tool("request_update", "Request that a provider's documentation be added,
279
279
  qdb.close();
280
280
  }
281
281
  });
282
+ // Tool: resolve_entity
283
+ const REEP_API = "https://reep-api.rahulkeerthi2-95d.workers.dev";
284
+ server.tool("resolve_entity", "Resolve a football entity (player, team, or coach) to get cross-provider IDs. Use when you need to map between Transfermarkt, FBref, Sofascore, Opta, and other provider IDs, or when you need to look up a player/team/coach by name.", {
285
+ name: z.string().optional().describe("Entity name to search for (e.g. 'Cole Palmer', 'Arsenal'). Fuzzy match on name and aliases."),
286
+ provider: z.string().optional().describe("Source provider for ID resolution (e.g. 'transfermarkt', 'fbref', 'sofascore', 'opta', 'soccerway')"),
287
+ id: z.string().optional().describe("ID from the source provider to resolve to all other IDs"),
288
+ qid: z.string().optional().describe("Wikidata QID for direct lookup (e.g. 'Q99760796')"),
289
+ type: z.enum(["player", "team", "coach"]).optional().describe("Filter results by entity type"),
290
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async ({ name, provider, id, qid, type }) => {
291
+ let url;
292
+ if (qid) {
293
+ url = `${REEP_API}/lookup?qid=${encodeURIComponent(qid)}`;
294
+ }
295
+ else if (provider && id) {
296
+ url = `${REEP_API}/resolve?provider=${encodeURIComponent(provider)}&id=${encodeURIComponent(id)}`;
297
+ }
298
+ else if (name) {
299
+ url = `${REEP_API}/search?name=${encodeURIComponent(name)}&limit=10`;
300
+ if (type)
301
+ url += `&type=${encodeURIComponent(type)}`;
302
+ }
303
+ else {
304
+ return {
305
+ isError: true,
306
+ content: [{
307
+ type: "text",
308
+ text: "Provide at least one of: name, qid, or provider+id. Examples:\n- name: 'Cole Palmer'\n- provider: 'transfermarkt', id: '568177'\n- qid: 'Q99760796'",
309
+ }],
310
+ };
311
+ }
312
+ try {
313
+ const resp = await fetch(url);
314
+ if (!resp.ok) {
315
+ return {
316
+ isError: true,
317
+ content: [{ type: "text", text: `Reep API error: ${resp.status} ${resp.statusText}` }],
318
+ };
319
+ }
320
+ const data = await resp.json();
321
+ if (!data.results?.length) {
322
+ return {
323
+ content: [{ type: "text", text: "No entities found matching the query." }],
324
+ };
325
+ }
326
+ const formatted = data.results.map((e) => {
327
+ const ids = e.external_ids;
328
+ const idLines = ids
329
+ ? Object.entries(ids).map(([k, v]) => ` ${k}: ${v}`).join("\n")
330
+ : " (none)";
331
+ const bio = [
332
+ e.date_of_birth && `DOB: ${e.date_of_birth}`,
333
+ e.nationality && `Nationality: ${e.nationality}`,
334
+ e.position && `Position: ${e.position}`,
335
+ e.height_cm && `Height: ${e.height_cm}cm`,
336
+ e.country && `Country: ${e.country}`,
337
+ e.stadium && `Stadium: ${e.stadium}`,
338
+ ].filter(Boolean).join(" | ");
339
+ return `### ${e.name_en} (${e.type})\nWikidata: ${e.qid}${e.aliases_en ? `\nAliases: ${e.aliases_en}` : ""}${bio ? `\n${bio}` : ""}\nProvider IDs:\n${idLines}`;
340
+ }).join("\n\n");
341
+ return {
342
+ content: [{
343
+ type: "text",
344
+ text: `Found ${data.results.length} result(s):\n\n${formatted}`,
345
+ }],
346
+ };
347
+ }
348
+ catch (err) {
349
+ return {
350
+ isError: true,
351
+ content: [{ type: "text", text: `Failed to reach Reep API: ${err instanceof Error ? err.message : String(err)}` }],
352
+ };
353
+ }
354
+ });
282
355
  // ── Start ───────────────────────────────────────────────────────────────
283
356
  async function main() {
284
357
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "football-docs",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Searchable football data provider documentation for AI coding agents. Like Context7 for football data.",
5
5
  "type": "module",
6
+ "mcpName": "io.github.withqwerty/football-docs",
6
7
  "bin": {
7
8
  "football-docs": "bin/serve.js"
8
9
  },