openapi: 3.1.0
info:
  title: Afore Public API
  description: |
    Read-only HTTP endpoints for machine clients (AI assistants, MCP tools, integrations).
    Does not expose full paid Library guide content — use catalog + public URLs only.
  version: "1.0.0"
  contact:
    url: https://afo.re/plan
servers:
  - url: https://afo.re
    description: Production
  - url: http://localhost:3000
    description: Local dev
tags:
  - name: Catalog
    description: CMS-backed listings (JSON)
  - name: Markdown
    description: Article and guide-metadata views in Markdown for LLM consumption
  - name: MCP
    description: Model Context Protocol endpoints for AI assistants
  - name: Structured datasets
    description: Per-guide structured data (places, films, drive times)
  - name: Integrations
    description: Proxied partner data
paths:
  /api/public/v1/journal:
    get:
      tags: [Catalog]
      summary: List Le Journal articles
      description: Title, slug, excerpt, dates, category, canonical URL per article.
      operationId: listJournalArticles
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  articles:
                    type: array
                    items:
                      type: object
                      properties:
                        title: { type: string }
                        slug: { type: string }
                        excerpt: { type: string }
                        publishedAt: { type: string }
                        category: { type: string }
                        authorName: { type: string }
                        url: { type: string, format: uri }
        "503":
          description: Sanity not configured
  /api/public/v1/library:
    get:
      tags: [Catalog]
      summary: List Library guides (metadata)
      description: >-
        Guide titles, slugs, blurbs, pricing hint — not full guide body.
      operationId: listLibraryGuides
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  guides:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "503":
          description: Sanity not configured
  /api/public/v1/journal/rss:
    get:
      tags: [Catalog]
      summary: RSS 2.0 feed for Le Journal
      operationId: journalRssFeed
      responses:
        "200":
          description: RSS XML
          content:
            application/rss+xml:
              schema:
                type: string
        "503":
          description: Sanity not configured
  /api/snowsure-resorts:
    get:
      tags: [Integrations]
      summary: SnowSure snow report (proxied)
      description: Cached proxy of SnowSure public API; shape follows upstream.
      operationId: getSnowsureResorts
      responses:
        "200":
          description: Upstream payload
        "502":
          description: Upstream error
  /api/luxski-concierge:
    get:
      tags: [Integrations]
      summary: LUXSKI Powder Pick (proxied)
      description: Daily featured hotel + snow outlook for Lux Ski marketing page.
      operationId: getLuxskiConcierge
      responses:
        "200":
          description: Normalized concierge payload
        "502":
          description: Upstream error
  /journal/{slug}.md:
    get:
      tags: [Markdown]
      summary: Le Journal article body in Markdown
      description: >-
        Full editorial text of one article rendered as Markdown with YAML
        front-matter (title, author, date, category, tags, canonical_url,
        license). Suitable for direct LLM ingestion.
      operationId: getJournalArticleMarkdown
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string, pattern: "^[a-z0-9-]+$" }
      responses:
        "200":
          description: Markdown body
          content:
            text/markdown:
              schema: { type: string }
        "404":
          description: No article with that slug
  /library/{slug}.md:
    get:
      tags: [Markdown]
      summary: Library guide public metadata in Markdown
      description: >-
        Guide title, tagline, description, "what's inside", "who it's for",
        free/paid status, and pointers to machine-readable sub-resources.
        Paid editorial bodies are NEVER returned by this endpoint.
      operationId: getLibraryGuideMarkdown
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string, pattern: "^[a-z0-9-]+$" }
      responses:
        "200":
          description: Markdown body
          content:
            text/markdown:
              schema: { type: string }
        "404":
          description: No guide with that slug
  /api/public/v1/library/{slug}/places:
    get:
      tags: [Structured datasets]
      summary: Structured places dataset for a guide
      description: >-
        Per-guide structured place dataset (currently available for
        cannes-film-locations). Each place carries id, name, area, GPS
        coordinates, the films that touched it, an in-page anchor URL,
        and per-photo licensing notes. Free metadata, suitable for maps
        and itinerary planners.
      operationId: getGuidePlaces
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string, pattern: "^[a-z0-9-]+$" }
      responses:
        "200":
          description: Structured dataset
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          description: No structured dataset for that guide slug
  /api/mcp:
    get:
      tags: [MCP]
      summary: Remote MCP endpoint (Streamable HTTP)
      description: >-
        Model Context Protocol endpoint over Streamable HTTP. Connect any
        MCP-compatible client (Claude Desktop, Cursor, ChatGPT desktop,
        Cline, Continue, Zed, etc.) by pasting the URL into the client's
        MCP server settings.
      operationId: mcpStreamableHttp
      responses:
        "200":
          description: MCP session
    post:
      tags: [MCP]
      summary: Remote MCP endpoint (Streamable HTTP)
      operationId: mcpStreamableHttpPost
      responses:
        "200":
          description: MCP session
  /api/sse:
    get:
      tags: [MCP]
      summary: Remote MCP endpoint (SSE, legacy)
      description: >-
        Server-Sent Events transport for MCP. Use Streamable HTTP at /api/mcp
        if your client supports it (most modern clients do).
      operationId: mcpSse
      responses:
        "200":
          description: SSE stream
  /.well-known/mcp.json:
    get:
      tags: [MCP]
      summary: MCP service discovery descriptor
      description: >-
        Lists Afore's MCP transports, authentication, and contact details
        for clients that auto-discover MCP servers.
      operationId: mcpDiscovery
      responses:
        "200":
          description: Descriptor JSON
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
