{"openapi":"3.1.0","info":{"title":"Learn, Speak, Repeat! machine API","version":"1.0.0","description":"OAuth-protected MCP sessions and public recording links. Versioning: The MCP surface is versioned by the MCP protocol revision header. Tool names and inputs are stable within major version 1; breaking changes ship as new tool names and are announced on /developers."},"servers":[{"url":"https://learnspeakrepeat.com"}],"paths":{"/mcp":{"post":{"operationId":"mcpRequest","summary":"Call the Spanish session MCP server","description":"MCP Streamable HTTP. Send a JSON-RPC 2.0 initialize request, tools/list, or tools/call with a bearer token. Responses can be JSON or server-sent events.","security":[{"oauth2":["sesion"]}],"parameters":[{"name":"MCP-Protocol-Version","in":"header","required":false,"schema":{"type":"string"},"example":"2025-11-25","description":"The MCP protocol revision negotiated at initialize; the server supports the current revision and stateless fallback for earlier ones."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"const":"2.0"},"method":{"type":"string"},"id":{"oneOf":[{"type":"string"},{"type":"integer"}]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"JSON-RPC response or event stream","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"const":"2.0"},"id":{},"result":{"type":"object"},"error":{"type":"object"}}}},"text/event-stream":{"schema":{"type":"string"}}}},"202":{"description":"Notification accepted"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/.well-known/oauth-protected-resource/mcp":{"get":{"operationId":"getProtectedResourceMetadata","summary":"Discover MCP OAuth resource metadata","description":"OAuth protected resource metadata for the MCP endpoint.","responses":{"200":{"description":"Resource metadata","content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string","format":"uri"},"authorization_servers":{"type":"array","items":{"type":"string","format":"uri"}},"resource_name":{"type":"string"}}}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"operationId":"getAuthorizationServerMetadata","summary":"Discover OAuth endpoints","description":"Authorization server metadata, including PKCE support and dynamic client registration.","responses":{"200":{"description":"Authorization server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"issuer":{"type":"string"},"authorization_endpoint":{"type":"string"},"token_endpoint":{"type":"string"},"registration_endpoint":{"type":"string"}}}}}}}}},"/oauth/register":{"post":{"operationId":"registerOAuthClient","summary":"Register an OAuth client","description":"RFC 7591 dynamic client registration. Register redirect URIs before starting authorization.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirect_uris"],"properties":{"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"client_name":{"type":"string"}}}}}},"responses":{"201":{"description":"Registered client","content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"OAuth protocol error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/oauth/token":{"post":{"operationId":"exchangeOAuthToken","summary":"Exchange or refresh a token","description":"OAuth authorization_code with PKCE or refresh_token grant. Form encoded request.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","client_id"],"properties":{"grant_type":{"enum":["authorization_code","refresh_token"]},"client_id":{"type":"string"},"code":{"type":"string"},"code_verifier":{"type":"string"},"redirect_uri":{"type":"string"},"refresh_token":{"type":"string"}}}}}},"responses":{"200":{"description":"Access and refresh tokens","content":{"application/json":{"schema":{"type":"object","required":["access_token","token_type"],"properties":{"access_token":{"type":"string"},"token_type":{"type":"string"},"expires_in":{"type":"integer"},"refresh_token":{"type":"string"},"scope":{"type":"string"}}}}}},"400":{"description":"OAuth protocol error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/public/recording/{token}":{"get":{"operationId":"getPublicRecording","summary":"Read a recording link","description":"Read task and result for an unexpired capability link. No bearer token is needed. Limit: 60 requests per minute per link.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"},"description":"Single-use recording link token"}],"responses":{"200":{"description":"Recording task and result","content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string"},"status":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"task":{"type":"object"},"result":{"type":["object","null"]},"error":{"type":["string","null"]}}}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Recording link rate limit exceeded: 60 requests per minute per link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}},"RateLimit":{"description":"Limit, remaining requests, and reset in seconds","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}}}}},"post":{"operationId":"submitPublicRecording","summary":"Submit audio or a typed dictation","description":"Submit one recording or typed dictation for an open capability link. The link is single-use.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"},"description":"Single-use recording link token"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["audio","durationSec"],"properties":{"audio":{"type":"string","format":"binary"},"durationSec":{"type":"number","exclusiveMinimum":0,"maximum":150}}}},"application/json":{"schema":{"type":"object","required":["answer"],"additionalProperties":false,"properties":{"answer":{"type":"string","minLength":1,"maxLength":2000}}}}}},"responses":{"200":{"description":"Processed recording or dictation","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"done"},"result":{"type":"object"}}}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Slot already submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Audio too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Recording link rate limit exceeded: 60 requests per minute per link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}},"RateLimit":{"description":"Limit, remaining requests, and reset in seconds","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}}}}}},"/api/public/recording/{token}/audio":{"get":{"operationId":"getPublicRecordingAudio","summary":"Play task audio","description":"Get generated task audio for an unexpired recording link.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"},"description":"Single-use recording link token"}],"responses":{"200":{"description":"Audio bytes","content":{"audio/mpeg":{"schema":{"type":"string","format":"binary"}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Recording link rate limit exceeded: 60 requests per minute per link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}},"RateLimit":{"description":"Limit, remaining requests, and reset in seconds","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Spanish, human-readable"},"code":{"type":"string","description":"Stable machine code when present, e.g. subscription_required, voice_minutes_exhausted"}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}}},"responses":{"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Authentication required or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Link expired or not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"MCP rate limit exceeded: 120 requests per minute per learner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"description":"Requests per 60-second window","schema":{"type":"string"}},"RateLimit":{"description":"Limit, remaining requests, and reset in seconds","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}}}},"securitySchemes":{"oauth2":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https://learnspeakrepeat.com/conectar","tokenUrl":"https://learnspeakrepeat.com/oauth/token","refreshUrl":"https://learnspeakrepeat.com/oauth/token","scopes":{"sesion":"Run the learner's daily Spanish sessions: read today's plan and exercises, submit answers for grading, create recording links."}}}}}}}