{
  "openapi": "3.1.0",
  "info": {
    "title": "Aardwolf Tutor learning stack",
    "version": "1.0.0",
    "description": "Save one explicit phrase and its conversation-specific meaning to the connected user's stack, then inspect genuine preparation status. Optionally supply a complete learner explanation from your own knowledge in detailedNotes (usage, grammar, register, gender/forms where relevant); avoid unsupported claims and never invent audio or word timings. The entire encoded request body is limited to 64 KiB. The server supplies account identity and voice settings. Website cookies do not authorize this API. OAuth access tokens must target resource https://aardwolftutor.com/mcp."
  },
  "servers": [{ "url": "https://aardwolftutor.com" }],
  "paths": {
    "/api/chatbot/stack": {
      "post": {
        "operationId": "add_to_stack",
        "summary": "Add one exact phrase to the connected user's stack",
        "description": "Call only when the user asks to save a phrase. Resolve 'that phrase' or 'the last phrase' from conversation only when exactly one phrase and its study language are unambiguous; otherwise ask the user. Supply the intended meaning from conversation; ask if it is uncertain. Never rewrite the phrase, infer study language from location, or send userId, bucketId, audio, or timing data. Supply meaningLanguageCode matching the supplied meaning; omit it only when you know the meaning is in the connected user's profile-default language. Generate one stable idempotencyKey for the save intent and reuse it on retries with identical content. Pending means accepted for preparation, NOT ready or playable. Claim ready only when status is ready. Respect retryAfterUtc when checking status.",
        "x-openai-isConsequential": true,
        "security": [{ "aardwolfOAuth": ["stack:write"] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddPhraseRequest" } } }
        },
        "responses": {
          "200": { "description": "Saved or replayed result; inspect status, which can be ready or failed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddPhraseResult" } } } },
          "202": { "description": "Accepted and saved; required preparation is pending, NOT ready to play.", "headers": { "Location": { "description": "User-owned job status endpoint", "schema": { "type": "string" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddPhraseResult" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "description": "The idempotency key was reused with different content, or a previous submission needs reconciliation. Do not invent a new key to bypass an uncertain submission.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "413": { "description": "The encoded request exceeds 64 KiB. Shorten optional notes before submitting a new save intent." },
          "429": { "description": "Submission limit reached. Wait before retrying the same save intent.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/api/chatbot/stack/{jobId}": {
      "get": {
        "operationId": "get_add_status",
        "summary": "Check preparation status of a previously submitted phrase",
        "description": "Use only the jobId returned by add_to_stack. Only the connected user's jobs are visible. Respect retryAfterUtc; do not rapidly poll. Pending is not ready to play; failed requires attention. Report the returned message and playerUrl. Never claim audio or timings are ready before status is ready.",
        "security": [{ "aardwolfOAuth": ["stack:read"] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "description": "Job identifier returned by add_to_stack", "schema": { "type": "integer", "minimum": 1 } }],
        "responses": {
          "200": { "description": "The current user-owned preparation state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddPhraseResult" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "No job visible to this account; no other user's data is returned.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Too many requests. Honor Retry-After and wait at least 60 seconds before checking again.", "headers": { "Retry-After": { "schema": { "type": "integer" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "aardwolfOAuth": {
        "type": "oauth2",
        "description": "Authorization code flow with PKCE S256. Use resource=https://aardwolftutor.com/mcp on authorization and token requests. Register an exact callback URI and a client ID with Aardwolf Tutor; never use a website cookie as an API credential. For persistent access, register authorization_code and refresh_token grant types and request offline_access along with the required stack scopes. Access tokens last 10 minutes; securely replace rotated refresh tokens after every refresh and refresh within 14 days. Reconnect after the 30-day authorization expires or access is revoked; do not loop on invalid_grant.",
        "flows": { "authorizationCode": { "authorizationUrl": "https://aardwolftutor.com/connect/authorize", "tokenUrl": "https://aardwolftutor.com/connect/token", "scopes": { "stack:write": "Save an explicit phrase to your learning stack", "stack:read": "Check preparation status of phrases added through this integration", "offline_access": "Keep the connection available using rotating refresh tokens until authorization expires or is revoked" } } }
      }
    },
    "schemas": {
      "AddPhraseRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["phrase", "meaning", "studyLanguageCode", "idempotencyKey"],
        "properties": {
          "phrase": { "type": "string", "minLength": 1, "maxLength": 320, "description": "The exact single phrase the user requested, plain text without HTML/control characters. Preserve its wording and internal spaces." },
          "meaning": { "type": "string", "minLength": 1, "maxLength": 320, "description": "The intended meaning/translation from the conversation, plain text. Clarify if ambiguous." },
          "studyLanguageCode": { "type": "string", "maxLength": 8, "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,4})?$", "description": "Explicit supported study-language code, e.g. th, en, ja. Never autodetect." },
          "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]{1,128}$", "description": "Stable unique key for this save intent, generated once and reused with identical input on retries." },
          "meaningLanguageCode": { "type": "string", "maxLength": 8, "pattern": "^[a-z]{2,3}(?:-[a-z0-9]{2,4})?$", "description": "Language code matching the supplied meaning. Omit only when you know it matches the connected user's profile-default meaning language." },
          "pronunciation": { "type": "string", "maxLength": 640, "description": "Optional pronunciation text supplied in context, not audio or timings." },
          "notes": { "type": "string", "maxLength": 2000, "description": "Optional brief plain-text learning context." },
          "detailedNotes": { "type": "string", "maxLength": 6000, "description": "Optional complete learner explanation from your own knowledge: usage, grammar, register, gender/forms where relevant. Plain text; newlines allowed. Avoid unsupported claims and never invent audio or word timings. Keep the entire encoded request under 64 KiB." }
        }
      },
      "AddPhraseResult": {
        "type": "object",
        "required": ["jobId", "phraseLinkId", "bucketId", "status", "pipelineStatus", "message", "alreadyExisted", "isFinished", "playerUrl", "retryAfterUtc"],
        "properties": {
          "jobId": { "type": "integer" },
          "phraseLinkId": { "type": ["integer", "null"] },
          "bucketId": { "type": ["integer", "null"] },
          "status": { "type": "string", "enum": ["pending", "ready", "failed"], "description": "Only ready confirms preparation is complete. Pending is accepted but not ready; failed requires attention." },
          "pipelineStatus": { "type": "string", "description": "Detailed server preparation state; never infer completion from this instead of status." },
          "message": { "type": "string" },
          "alreadyExisted": { "type": "boolean" },
          "isFinished": { "type": "boolean" },
          "playerUrl": { "type": "string", "description": "Open the learning stack in Aardwolf Tutor; the URL alone is not evidence of readiness." },
          "retryAfterUtc": { "type": ["string", "null"], "format": "date-time", "description": "Do not check status before this time. If absent for a pending job, wait at least 10 seconds." }
        }
      },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" }, "message": { "type": "string", "description": "Optional explanation; transport-level failures may return only an error code." } } }
    },
    "responses": {
      "InvalidRequest": { "description": "Missing, ambiguous, unsupported, or invalid input. Clarify with the user; do not guess.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "A valid scoped OAuth bearer token is required. Connect the user's account.", "headers": { "WWW-Authenticate": { "schema": { "type": "string" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "The token does not grant this operation. Reconnect with the required scope.", "headers": { "WWW-Authenticate": { "schema": { "type": "string" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unavailable": { "description": "Chatbot connections are not enabled or are unavailable. Do not claim a phrase was saved." }
    }
  }
}
