{
  "openapi": "3.1.0",
  "info": {
    "title": "FPLai Agent API",
    "version": "2.0.0",
    "summary": "Read-only discovery API for FPLai, the AI assistant for Fantasy Premier League managers.",
    "description": "Read-only endpoints for discovering FPLai content, season hubs, and agent-facing metadata. Every public HTML page on fplai.app also serves Markdown when requested with `Accept: text/markdown`. This document is published at https://fplai.app/openapi.json and https://fplai.app/.well-known/openapi/agent-api.json.\n\n**Versioning.** The canonical surface is version-in-path: `/api/v1/agent/*`. The older unversioned paths `/api/agent/health` and `/api/agent/content-index` remain available as compatibility aliases and return identical payloads, but they are deprecated: their responses carry an RFC 9745 `Deprecation` header and a `Link: <...>; rel=\"successor-version\"` header pointing at the matching `/api/v1` path. No `Sunset` header is advertised because no removal date has been decided; when one is, a `Sunset` header will be published at least 12 months before the aliases are withdrawn. Breaking changes ship as a new path version (`/api/v2/...`); additive fields may appear inside `v1` without a version bump.\n\n**Errors.** Every non-2xx response is an RFC 9457 problem detail served as `application/problem+json`, with a stable machine-readable `code` and a `type` URI under `https://fplai.app/problems/`.\n\n**Rate limits.** Requests are metered per client IP at 60 requests per 60 seconds. Responses carry both the IETF `RateLimit-Policy`/`RateLimit` structured fields and the legacy `RateLimit-Limit`/`RateLimit-Remaining`/`RateLimit-Reset` trio. Enforcement is best-effort and per edge isolate (see `x-rate-limit`).",
    "termsOfService": "https://fplai.app/terms",
    "contact": {
      "name": "FPLai",
      "url": "https://fplai.app/support",
      "email": "hello@fplai.app"
    }
  },
  "externalDocs": {
    "description": "FPLai agent guide (Markdown)",
    "url": "https://fplai.app/.well-known/service-docs/agent-guide.md"
  },
  "servers": [
    {
      "url": "https://fplai.app"
    }
  ],
  "x-versioning": {
    "scheme": "url-path",
    "current": "v1",
    "basePath": "/api/v1",
    "deprecatedAliases": [
      {
        "path": "/api/agent/health",
        "successor": "/api/v1/agent/health",
        "deprecatedOn": "2026-09-09"
      },
      {
        "path": "/api/agent/content-index",
        "successor": "/api/v1/agent/content-index",
        "deprecatedOn": "2026-09-09"
      }
    ],
    "deprecationSignalling": [
      "Deprecation header (RFC 9745)",
      "Link header with rel=\"successor-version\" (RFC 8288)"
    ],
    "removalPolicy": "A Sunset header (RFC 8594) will be published at least 12 months before any deprecated path is removed. No Sunset date is currently set.",
    "breakingChangePolicy": "Breaking changes ship under a new path version. Additive, backward-compatible fields may appear within an existing version."
  },
  "x-rate-limit": {
    "policy": "agent-api",
    "quota": 60,
    "windowSeconds": 60,
    "key": "client IP (CF-Connecting-IP, else the first hop of X-Forwarded-For)",
    "appliesTo": ["/api/v1/agent/*", "/api/agent/*", "/mcp", "/a2a"],
    "enforcement": "best-effort, per edge isolate",
    "notes": "Counters live in the memory of the Cloudflare edge isolate that serves the request, so the effective global limit is higher than the published quota and resets when an isolate recycles. Treat the RateLimit headers as cooperative signalling, not as a hard guarantee.",
    "headers": [
      "RateLimit-Policy",
      "RateLimit",
      "RateLimit-Limit",
      "RateLimit-Remaining",
      "RateLimit-Reset",
      "Retry-After"
    ]
  },
  "tags": [
    { "name": "discovery", "description": "Health and content-index endpoints" },
    { "name": "mcp", "description": "Read-only Model Context Protocol endpoint" }
  ],
  "components": {
    "headers": {
      "RateLimitPolicy": {
        "description": "IETF structured-field rate-limit policy, e.g. `\"agent-api\";q=60;w=60`.",
        "schema": { "type": "string" },
        "example": "\"agent-api\";q=60;w=60"
      },
      "RateLimit": {
        "description": "IETF structured-field rate-limit state: `r` is the remaining quota, `t` the seconds until the window resets.",
        "schema": { "type": "string" },
        "example": "\"agent-api\";r=59;t=60"
      },
      "RateLimitLimit": {
        "description": "Legacy rate-limit quota for the current window.",
        "schema": { "type": "integer" },
        "example": 60
      },
      "RateLimitRemaining": {
        "description": "Legacy count of requests still available in the current window.",
        "schema": { "type": "integer" },
        "example": 59
      },
      "RateLimitReset": {
        "description": "Legacy seconds until the current window resets.",
        "schema": { "type": "integer" },
        "example": 60
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying after a 429.",
        "schema": { "type": "integer" },
        "example": 42
      },
      "Deprecation": {
        "description": "RFC 9745 deprecation marker on the unversioned aliases: an `@`-prefixed epoch second.",
        "schema": { "type": "string" },
        "example": "@1788912000"
      },
      "SuccessorVersionLink": {
        "description": "RFC 8288 Link header. Always contains `rel=\"service-desc\"`; deprecated aliases add `rel=\"successor-version\"`.",
        "schema": { "type": "string" },
        "example": "</api/v1/agent/health>; rel=\"successor-version\""
      }
    },
    "parameters": {
      "Season": {
        "in": "query",
        "name": "season",
        "required": false,
        "description": "Restrict matches to one FPL season slug, formatted `YYYY-YY` (for example `2025-26`). Any other shape returns a 400 problem.",
        "schema": { "type": "string", "pattern": "^\\d{4}-\\d{2}$" },
        "example": "2025-26"
      },
      "Family": {
        "in": "query",
        "name": "family",
        "required": false,
        "description": "Restrict matches to one content family slug, such as `captain-picks`, `compare`, or `replacement`. Use the `families` array of an unfiltered response to discover valid values.",
        "schema": { "type": "string", "pattern": "^[a-z0-9-]{1,64}$" },
        "example": "captain-picks"
      },
      "Query": {
        "in": "query",
        "name": "q",
        "required": false,
        "description": "Case-insensitive substring searched across each entry's path, title, family, and season.",
        "schema": { "type": "string", "maxLength": 128 },
        "example": "haaland"
      },
      "Limit": {
        "in": "query",
        "name": "limit",
        "required": false,
        "description": "Maximum number of entries returned in `matches`. Must be an integer between 1 and 100; anything else returns a 400 problem.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 },
        "example": 25
      },
      "Cursor": {
        "in": "query",
        "name": "cursor",
        "required": false,
        "description": "Opaque pagination cursor. Omit it for the first page, then pass the `nextCursor` of the previous response to continue. A cursor is only valid for the filter set it was issued with: changing `season`, `family`, or `q` (or sending a value this API did not mint) returns a 400 problem with code `invalid_query`. `nextCursor` is `null` on the last page.",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,256}$" },
        "example": "MjUuM2htcnA"
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail, served as `application/problem+json`.",
        "required": ["type", "title", "status", "code"],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Stable URI identifying the problem class, under https://fplai.app/problems/.",
            "example": "https://fplai.app/problems/not-found"
          },
          "title": { "type": "string", "description": "Short, human-readable summary of the problem class." },
          "status": { "type": "integer", "description": "HTTP status code, repeated for clients that only read the body." },
          "detail": { "type": "string", "description": "Human-readable explanation specific to this occurrence." },
          "instance": { "type": "string", "description": "Path of the request that produced the problem." },
          "code": {
            "type": "string",
            "description": "Machine-readable problem code. Branch on this rather than parsing the type URI.",
            "enum": ["not_found", "method_not_allowed", "invalid_query", "rate_limited", "internal_error"]
          }
        }
      },
      "AgentHealth": {
        "type": "object",
        "description": "Liveness and self-description of the public agent surface.",
        "required": ["ok", "site", "mcp", "generatedAt", "apiVersion"],
        "properties": {
          "ok": { "type": "boolean", "description": "True when the Worker is serving the agent API." },
          "site": { "type": "string", "format": "uri", "description": "Canonical site origin.", "example": "https://fplai.app" },
          "mcp": { "type": "string", "format": "uri", "description": "Read-only MCP endpoint.", "example": "https://fplai.app/mcp" },
          "generatedAt": { "type": "string", "format": "date-time", "description": "Server time when the response was produced." },
          "apiVersion": { "type": "string", "description": "Path version serving this response.", "example": "v1" }
        }
      },
      "ContentIndexEntry": {
        "type": "object",
        "description": "One public FPLai route.",
        "required": ["path", "season", "family", "indexable", "title"],
        "properties": {
          "path": { "type": "string", "description": "Same-origin pathname, always with a trailing slash.", "example": "/fpl/2025-26/captain-picks/gw33/" },
          "season": { "type": ["string", "null"], "description": "Season slug parsed from the path, or null for season-independent routes.", "example": "2025-26" },
          "family": { "type": "string", "description": "Content family parsed from the path.", "example": "captain-picks" },
          "indexable": { "type": "boolean", "description": "False when the page is published but marked noindex." },
          "title": { "type": "string", "description": "Human-readable title derived from the path." }
        }
      },
      "ContentIndexFamily": {
        "type": "object",
        "required": ["name", "count"],
        "properties": {
          "name": { "type": "string", "description": "Content family slug." },
          "count": { "type": "integer", "description": "Number of published routes in this family, before filtering." }
        }
      },
      "ContentIndex": {
        "type": "object",
        "description": "Filtered index of the published FPLai content routes.",
        "required": ["site", "generatedAt", "filters", "totals", "pageSize", "nextCursor", "seasons", "families", "matches"],
        "properties": {
          "site": { "type": "string", "format": "uri", "example": "https://fplai.app" },
          "generatedAt": { "type": "string", "format": "date-time" },
          "filters": {
            "type": "object",
            "description": "Echo of the applied query, with defaults resolved.",
            "required": ["season", "family", "q", "limit", "cursor"],
            "properties": {
              "season": { "type": ["string", "null"] },
              "family": { "type": ["string", "null"] },
              "q": { "type": ["string", "null"] },
              "limit": { "type": "integer", "minimum": 1, "maximum": 100 },
              "cursor": { "type": ["string", "null"], "description": "The cursor the caller sent, or null on a first page." }
            }
          },
          "totals": {
            "type": "object",
            "required": ["allEntries", "matchedEntries"],
            "properties": {
              "allEntries": { "type": "integer", "description": "Total published routes known to the Worker." },
              "matchedEntries": { "type": "integer", "description": "Routes matching the filters, before `limit` is applied." }
            }
          },
          "pageSize": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Number of entries `matches` can hold on this page; equal to the resolved `limit`."
          },
          "nextCursor": {
            "type": ["string", "null"],
            "description": "Pass this back as `cursor` to fetch the next page, or null when this is the last page.",
            "example": null
          },
          "seasons": {
            "type": "array",
            "description": "All known season slugs, newest first.",
            "items": { "type": "string" }
          },
          "families": {
            "type": "array",
            "description": "All content families with their unfiltered counts, sorted by name.",
            "items": { "$ref": "#/components/schemas/ContentIndexFamily" }
          },
          "matches": {
            "type": "array",
            "description": "One page of matching routes: at most `pageSize` entries, starting at the position encoded in `filters.cursor`.",
            "items": { "$ref": "#/components/schemas/ContentIndexEntry" }
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "description": "JSON-RPC 2.0 request accepted by /mcp. `method` is one of `initialize`, `tools/list`, or `tools/call`; `params` carries `name` and `arguments` for `tools/call`.",
        "required": ["jsonrpc", "method"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "type": ["string", "integer", "null"], "description": "Request id echoed on the response." },
          "method": {
            "type": "string",
            "enum": ["initialize", "tools/list", "tools/call"],
            "example": "tools/call"
          },
          "params": {
            "type": "object",
            "description": "Method parameters. For `tools/call`: `{ \"name\": \"search_content_paths\", \"arguments\": { \"query\": \"haaland\" } }`.",
            "properties": {
              "name": { "type": "string", "description": "Tool name, for tools/call." },
              "arguments": {
                "type": "object",
                "description": "Tool arguments, validated against that tool's inputSchema from tools/list.",
                "additionalProperties": true
              }
            },
            "additionalProperties": true
          }
        },
        "additionalProperties": false
      },
      "JsonRpcResult": {
        "type": "object",
        "description": "Successful JSON-RPC 2.0 envelope. `result` is the method's payload: server capabilities for `initialize`, `{ tools: [...] }` for `tools/list`, or `{ content: [...], structuredContent: {...} }` for `tools/call`.",
        "required": ["jsonrpc", "id", "result"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "type": ["string", "integer", "null"] },
          "result": { "type": "object", "additionalProperties": true }
        },
        "additionalProperties": false
      },
      "JsonRpcResponse": {
        "description": "Either a JSON-RPC result envelope or a JSON-RPC error envelope; both are returned with HTTP 200 except for a parse error, which uses HTTP 400.",
        "oneOf": [
          { "$ref": "#/components/schemas/JsonRpcResult" },
          { "$ref": "#/components/schemas/JsonRpcError" }
        ]
      },
      "McpServerInfo": {
        "type": "object",
        "description": "MCP server description returned by `GET /mcp`, mirroring /.well-known/mcp/server-card.json.",
        "required": ["serverInfo", "transport", "endpoint"],
        "properties": {
          "serverInfo": {
            "type": "object",
            "required": ["name", "version"],
            "properties": {
              "name": { "type": "string", "example": "FPLai Content MCP" },
              "version": { "type": "string", "example": "1.0.0" }
            }
          },
          "transport": { "type": "string", "const": "streamable-http" },
          "endpoint": { "type": "string", "format": "uri", "example": "https://fplai.app/mcp" }
        },
        "additionalProperties": false
      },
      "JsonRpcError": {
        "type": "object",
        "description": "JSON-RPC 2.0 error envelope returned by /mcp. Transport-level failures use HTTP 400 for parse errors and HTTP 200 for JSON-RPC application errors, per the JSON-RPC convention.",
        "required": ["jsonrpc", "id", "error"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "type": ["string", "integer", "null"], "description": "Echo of the request id, or null when it could not be parsed." },
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "integer", "description": "JSON-RPC error code, e.g. -32700 parse error, -32601 method not found, -32602 invalid params." },
              "message": { "type": "string" }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid query parameter (code `invalid_query`).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "NotFound": {
        "description": "No such agent API endpoint (code `not_found`).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "MethodNotAllowed": {
        "description": "The agent API is read-only (code `method_not_allowed`).",
        "headers": {
          "Allow": { "description": "Methods this endpoint accepts.", "schema": { "type": "string" }, "example": "GET, HEAD, OPTIONS" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "TooManyRequests": {
        "description": "Rate limit exhausted (code `rate_limited`).",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
          "RateLimit": { "$ref": "#/components/headers/RateLimit" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "InternalError": {
        "description": "Unhandled Worker failure (code `internal_error`).",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "UnexpectedProblem": {
        "description": "Any other error, as an RFC 9457 problem detail.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    }
  },
  "paths": {
    "/api/v1/agent/health": {
      "get": {
        "operationId": "getAgentHealthV1",
        "tags": ["discovery"],
        "summary": "Check health for the public agent surface",
        "description": "Returns a small liveness document for the FPLai public agent surface: whether the Worker is serving requests, the canonical site origin, the MCP endpoint, the server time, and the path version answering the request. Use it as a cheap readiness probe before calling the content index or the MCP endpoint. Unauthenticated, read-only, and safe to poll within the published rate limit.",
        "responses": {
          "200": {
            "description": "Agent surface is healthy.",
            "headers": {
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "Link": { "$ref": "#/components/headers/SuccessorVersionLink" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentHealth" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      }
    },
    "/api/v1/agent/content-index": {
      "get": {
        "operationId": "getContentIndexV1",
        "tags": ["discovery"],
        "summary": "List seasons, content families, and matching pSEO routes",
        "description": "Returns the machine-readable index of every public FPLai content route, optionally filtered by season, content family, or a free-text substring. The response always includes the full list of known seasons and the per-family counts, so a single call is enough to discover the shape of the site before drilling in. Each match carries the same-origin path, which can then be fetched as HTML or, with `Accept: text/markdown`, as Markdown.",
        "parameters": [
          { "$ref": "#/components/parameters/Season" },
          { "$ref": "#/components/parameters/Family" },
          { "$ref": "#/components/parameters/Query" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "Content index response.",
            "headers": {
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "Link": { "$ref": "#/components/headers/SuccessorVersionLink" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentIndex" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      }
    },
    "/api/agent/health": {
      "get": {
        "operationId": "getAgentHealthDeprecated",
        "tags": ["discovery"],
        "deprecated": true,
        "summary": "Check health for the public agent surface (deprecated alias)",
        "description": "Deprecated compatibility alias for `getAgentHealthV1`. It returns the identical payload, but every response carries an RFC 9745 `Deprecation` header and a `Link: </api/v1/agent/health>; rel=\"successor-version\"` header. Migrate to `/api/v1/agent/health`. No removal date is set; a `Sunset` header will announce one at least 12 months in advance.",
        "responses": {
          "200": {
            "description": "Agent surface is healthy (deprecated path).",
            "headers": {
              "Deprecation": { "$ref": "#/components/headers/Deprecation" },
              "Link": { "$ref": "#/components/headers/SuccessorVersionLink" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentHealth" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      }
    },
    "/api/agent/content-index": {
      "get": {
        "operationId": "getContentIndexDeprecated",
        "tags": ["discovery"],
        "deprecated": true,
        "summary": "List seasons, content families, and matching pSEO routes (deprecated alias)",
        "description": "Deprecated compatibility alias for `getContentIndexV1`, accepting the same parameters and returning the same payload. Every response carries an RFC 9745 `Deprecation` header and a `Link: </api/v1/agent/content-index>; rel=\"successor-version\"` header. Migrate to `/api/v1/agent/content-index`. No removal date is set; a `Sunset` header will announce one at least 12 months in advance.",
        "parameters": [
          { "$ref": "#/components/parameters/Season" },
          { "$ref": "#/components/parameters/Family" },
          { "$ref": "#/components/parameters/Query" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "Content index response (deprecated path).",
            "headers": {
              "Deprecation": { "$ref": "#/components/headers/Deprecation" },
              "Link": { "$ref": "#/components/headers/SuccessorVersionLink" },
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContentIndex" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "callMcp",
        "tags": ["mcp"],
        "summary": "Send a JSON-RPC request to the read-only FPLai MCP endpoint",
        "description": "Streamable-HTTP Model Context Protocol endpoint exposing four read-only tools: `get_site_overview`, `list_content_sections`, `search_content_paths`, and `get_page_markdown`. Send JSON-RPC 2.0 requests (`initialize`, `tools/list`, `tools/call`). Application-level failures come back as a JSON-RPC error envelope with HTTP 200; an unparseable body returns HTTP 400 with the same envelope. Shares the agent API rate-limit policy, and rate-limit exhaustion returns an RFC 9457 problem rather than a JSON-RPC envelope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcRequest" } }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response (result envelope, or an error envelope for application-level failures).",
            "headers": {
              "RateLimit-Policy": { "$ref": "#/components/headers/RateLimitPolicy" },
              "RateLimit": { "$ref": "#/components/headers/RateLimit" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcResponse" } } }
          },
          "400": {
            "description": "Unparseable JSON body, returned as a JSON-RPC parse error (-32700).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonRpcError" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      },
      "get": {
        "operationId": "getMcpServerInfo",
        "tags": ["mcp"],
        "summary": "Describe the read-only FPLai MCP endpoint",
        "description": "Returns the MCP server card: server name and version, the transport in use (`streamable-http`), and the canonical endpoint URL. Useful for clients that probe an MCP endpoint with GET before opening a session.",
        "responses": {
          "200": {
            "description": "MCP server description.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/McpServerInfo" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalError" },
          "default": { "$ref": "#/components/responses/UnexpectedProblem" }
        }
      }
    }
  }
}
