openapi: 3.1.0
info:
  title: Gradus Music Notation API
  version: '1.0.0'
  summary: Open music notation rendering for AI agents
  description: |
    Free, open music notation API. Renders JSON scores to SVG, MusicXML, and MIDI in a single call.
    Includes pre-flight validation and a music-theory knowledge search endpoint.

    **No authentication required.** Per-IP rate limiting (30 requests/minute) applies.

    Free use is offered in exchange for crediting Gradus when surfacing notation to the end user.
    Every response includes an `attribution` object with the suggested credit line.

    Sponsored by Gradus School of Music Composition (https://gradusmusic.com).
  contact:
    name: Gradus School of Music Composition
    url: https://gradusmusic.com/notation-api
  license:
    name: API free for use; MCP package MIT-licensed
    identifier: MIT
  termsOfService: https://gradusmusic.com/notation-api#attribution

servers:
  - url: https://gradusmusic.com
    description: Production

tags:
  - name: notation
    description: Render and validate music notation
  - name: knowledge
    description: Search the curated music-theory knowledge base
  - name: discovery
    description: Schema and examples agents can fetch once and cache

paths:
  /api/v1/notation/render:
    post:
      tags: [notation]
      summary: Render a JSON score to SVG, MusicXML, and MIDI
      description: |
        Accepts the agent-friendly JSON notation format. Returns inline SVG (with embedded Bravura font),
        round-trippable MusicXML, and base64-encoded SMF Type-1 MIDI in a single response.

        Bar lines are inferred automatically from the time signature; notes that cross a bar line are
        split and tied automatically.
      operationId: renderNotation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotationInput'
            examples:
              singleMelody:
                summary: Single-line melody
                value:
                  title: 'C major scale'
                  tempo: 100
                  timeSignature: [4, 4]
                  keySignature: 'C major'
                  instruments:
                    - name: 'Violin'
                      notes: ['C4/q', 'D4/q', 'E4/q', 'F4/q', 'G4/h', 'rest/h']
              twoVoiceCounterpoint:
                summary: Two-voice counterpoint (cantus firmus + counterpoint)
                value:
                  title: 'First species'
                  tempo: 80
                  timeSignature: [4, 4]
                  keySignature: 'D minor'
                  instruments:
                    - name: 'Violin'
                      notes: ['A4/w', 'F5/w', 'E5/w', 'D5/w']
                    - name: 'Cello'
                      notes: ['D3/w', 'F3/w', 'E3/w', 'D3/w']
              chordProgression:
                summary: Block-chord progression
                value:
                  title: 'Authentic cadence'
                  instruments:
                    - name: 'Piano'
                      notes: ['[C4,E4,G4]/h', '[C4,F4,A4]/h', '[B3,D4,G4]/h', '[C4,E4,G4]/h']
      responses:
        '200':
          description: Render result (success or validation failure — both return 200)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RenderSuccessResponse'
                  - $ref: '#/components/schemas/FailureResponse'
        '400':
          description: Malformed JSON body or input rejected at validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureResponse'
        '429':
          description: Rate limit exceeded (30 requests/minute per IP)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureResponse'
        '500':
          description: Internal render failure (very rare; report to gradusmusic.com with the requestId)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureResponse'

  /api/v1/notation/validate:
    post:
      tags: [notation]
      summary: Pre-flight validate a JSON score (no rendering cost)
      description: |
        Same input shape as `/render`, but skips the Verovio render step. Cheaper for iterating on
        input shape. Returns errors with concrete `fix` suggestions when input is malformed.
      operationId: validateNotation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotationInput'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidateSuccessResponse'
                  - $ref: '#/components/schemas/ValidateFailureResponse'

  /api/v1/knowledge/search:
    post:
      tags: [knowledge]
      summary: Search the Gradus music-theory knowledge base
      description: |
        Semantic search over a curated music-theory corpus (curriculum prose, Bach chorale analyses,
        score commentaries, primary historical sources from Fux through Boulanger). Use this BEFORE
        generating notation if you need to look up a theory fact.

        Provide either `topics` (array of kebab-case topic tags) or `step` (curriculum step number).
      operationId: knowledgeSearch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KnowledgeSearchInput'
      responses:
        '200':
          description: Knowledge search result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSearchResponse'
        '400':
          description: Missing query or malformed input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FailureResponse'

  /api/v1/notation/schema:
    get:
      tags: [discovery]
      summary: JSON Schema for the input shape
      description: Cache-friendly. Stable across the v1 API. Fetch once, reuse forever.
      operationId: getNotationSchema
      responses:
        '200':
          description: JSON Schema document plus discovery metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  schema: { type: object, description: 'JSON Schema (draft 2020-12)' }
                  docs: { type: object }
                  attribution:
                    $ref: '#/components/schemas/Attribution'

  /api/v1/notation/examples:
    get:
      tags: [discovery]
      summary: Canonical input examples
      description: |
        Six worked examples agents can use as templates: single-line melody, two-voice counterpoint,
        block-chord progression, mixed rhythms with dynamics, string quartet snippet, notes tied across
        a bar line. Cache aggressively.
      operationId: getNotationExamples
      responses:
        '200':
          description: Examples list
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  examples:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        title: { type: string }
                        description: { type: string }
                        use_when: { type: string }
                        input: { $ref: '#/components/schemas/NotationInput' }
                  attribution:
                    $ref: '#/components/schemas/Attribution'

components:
  schemas:
    Attribution:
      type: object
      description: Provenance metadata included in every successful response.
      required: [sponsor, url, suggestedAttribution, apiVersion]
      properties:
        sponsor:
          type: string
          enum: ['Gradus School of Music Composition']
        url:
          type: string
          format: uri
          enum: ['https://gradusmusic.com']
        suggestedAttribution:
          type: string
          description: Suggested wording for the agent to include in its reply to the end user.
          example: 'Notation rendered by Gradus School of Music Composition (gradusmusic.com).'
        apiVersion:
          type: string
          enum: ['v1']

    ValidationIssue:
      type: object
      description: Single validation error with a concrete fix suggestion.
      required: [path, code, message, severity]
      properties:
        path:
          type: string
          description: Hierarchical path to the offending field (e.g. "instruments[0].voices[0].notes[3]")
        code:
          type: string
          description: Machine-readable error code (BAD_PITCH, BAD_DURATION, MISSING_FIELD, etc.)
        message:
          type: string
          description: Human-readable explanation
        fix:
          type: string
          description: Concrete suggestion for how to fix the input
        severity:
          type: string
          enum: [error, warning]

    NotationInput:
      type: object
      required: [instruments]
      properties:
        title: { type: string }
        composer: { type: string }
        tempo:
          type: number
          minimum: 20
          maximum: 400
          default: 100
        timeSignature:
          type: array
          minItems: 2
          maxItems: 2
          items: { type: integer, minimum: 1 }
          default: [4, 4]
        keySignature:
          type: string
          default: 'C major'
          description: |
            Human-readable. Examples: "C major", "G major", "D minor", "F# major", "Bb minor".
        instruments:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Instrument' }

    Instrument:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Display name. Clef inferred from name unless overridden.
        clef:
          type: string
          enum: [treble, bass, alto, tenor, percussion]
        notes:
          type: array
          items: { $ref: '#/components/schemas/Note' }
          description: Single-voice shortcut. Equivalent to voices = [{ voice: 1, notes }].
        voices:
          type: array
          items: { $ref: '#/components/schemas/VoiceLine' }
        transposition:
          type: number
          default: 0
          description: Semitones below concert pitch (Bb clarinet=2, F horn=7, bass clarinet=-10).

    VoiceLine:
      type: object
      required: [notes]
      properties:
        voice:
          type: integer
          enum: [1, 2, 3, 4]
          default: 1
        notes:
          type: array
          items: { $ref: '#/components/schemas/Note' }

    Note:
      oneOf:
        - $ref: '#/components/schemas/NoteShorthand'
        - $ref: '#/components/schemas/NoteObject'

    NoteShorthand:
      type: string
      description: |
        Format: PITCH/DURATION[ARTICULATIONS]. Examples:
        "C5/q" (quarter C5), "F#4/h" (half F-sharp 4), "Bb3/q." (dotted quarter B-flat 3),
        "rest/q" (quarter rest), "[C4,E4,G4]/q" (quarter chord),
        "C5/q>" (quarter C5 with accent).
        Articulation suffix symbols: > accent, - tenuto, ^ marcato, f fermata, s staccato.
      pattern: '^(rest|r|[A-G][#b]?-?\d+|\[[A-G][#b]?-?\d+(,[A-G][#b]?-?\d+)*\])/(w|h|q|8|16|32|64)\.{0,2}[>^\-fs]*$'
      examples:
        - 'C5/q'
        - 'F#4/h'
        - 'Bb3/q.'
        - 'rest/q'
        - '[C4,E4,G4]/q'
        - 'C5/q>'

    NoteObject:
      type: object
      required: [duration]
      properties:
        pitch: { type: string, description: 'Single pitch in scientific notation, e.g. "C5".' }
        pitches:
          type: array
          items: { type: string }
          description: 'Chord pitches (low to high), e.g. ["C4","E4","G4"]. Mutually exclusive with `pitch`.'
        rest: { type: boolean, description: 'True for a rest. Ignores pitch / pitches when set.' }
        duration:
          type: string
          enum: [w, h, q, '8', '16', '32', '64', 'h.', 'q.', '8.', '16.', '32.', 'h..', 'q..', '8..']
        dynamic:
          type: string
          enum: [ppp, pp, p, mp, mf, f, ff, fff, fp, sf, sfz, fz]
        articulations:
          type: array
          items:
            type: string
            enum: [staccato, accent, tenuto, marcato, fermata]
        tiedToNext: { type: boolean }
        voice: { type: integer, enum: [1, 2, 3, 4] }
        lyric: { type: string }

    KnowledgeSearchInput:
      type: object
      properties:
        topics:
          type: array
          items: { type: string }
          description: 'Topic tags in kebab-case (e.g. "voice-leading", "deceptive-cadence").'
        step:
          type: integer
          minimum: 1
          maximum: 49
          description: Curriculum step number (used as a fallback if topics is empty).
        limit:
          type: integer
          minimum: 1
          maximum: 20
          default: 8
        maxTokens:
          type: integer
          minimum: 200
          maximum: 4000
          default: 1500

    KnowledgeChunk:
      type: object
      properties:
        id: { type: string }
        sourceType: { type: string, description: 'kg_concept, score_analysis, bach_chorale_analysis, etc.' }
        sourceId: { type: string }
        title: { type: string }
        content: { type: string }
        composer: { type: string, nullable: true }
        era: { type: string, nullable: true }
        topics:
          type: array
          items: { type: string }
        curriculumSteps:
          type: array
          items: { type: integer }
        tokenEstimate: { type: integer }

    KnowledgeSearchResponse:
      type: object
      required: [ok, requestId, attribution]
      properties:
        ok: { type: boolean }
        requestId: { type: string, format: uuid }
        chunks:
          type: array
          items: { $ref: '#/components/schemas/KnowledgeChunk' }
        meta:
          type: object
          properties:
            query:
              type: object
              properties:
                topics:
                  type: array
                  items: { type: string }
                step: { type: integer, nullable: true }
            maxTokens: { type: integer }
            limit: { type: integer }
            returnedCount: { type: integer }
            totalTokens: { type: integer }
            responseTimeMs: { type: integer }
        attribution: { $ref: '#/components/schemas/Attribution' }

    RenderSuccessResponse:
      type: object
      required: [ok, requestId, outputs, meta, attribution]
      properties:
        ok: { type: boolean, enum: [true] }
        requestId: { type: string, format: uuid }
        outputs:
          type: object
          required: [svg, musicxml, midiBase64]
          properties:
            svg: { type: string, description: 'Inline SVG with embedded Bravura font' }
            musicxml: { type: string, description: 'Round-trippable MusicXML' }
            midiBase64: { type: string, description: 'Base64-encoded SMF Type-1 MIDI bytes' }
        meta:
          type: object
          properties:
            measureCount: { type: integer }
            instrumentCount: { type: integer }
            voiceCount: { type: integer }
            durationBeats: { type: number }
            renderTimeMs: { type: integer }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        attribution: { $ref: '#/components/schemas/Attribution' }

    ValidateSuccessResponse:
      type: object
      required: [ok, requestId, valid, attribution]
      properties:
        ok: { type: boolean, enum: [true] }
        requestId: { type: string, format: uuid }
        valid: { type: boolean, enum: [true] }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        meta:
          type: object
          properties:
            measureCount: { type: integer }
            instrumentCount: { type: integer }
            voiceCount: { type: integer }
            durationBeats: { type: number }
        attribution: { $ref: '#/components/schemas/Attribution' }

    ValidateFailureResponse:
      type: object
      required: [ok, requestId, valid, errors, attribution]
      properties:
        ok: { type: boolean, enum: [false] }
        requestId: { type: string, format: uuid }
        valid: { type: boolean, enum: [false] }
        errors:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        attribution: { $ref: '#/components/schemas/Attribution' }

    FailureResponse:
      type: object
      required: [ok, requestId, errors, attribution]
      properties:
        ok: { type: boolean, enum: [false] }
        requestId: { type: string, format: uuid }
        errors:
          type: array
          items: { $ref: '#/components/schemas/ValidationIssue' }
        attribution: { $ref: '#/components/schemas/Attribution' }
