Skip to main content
Glama

Lovely Composer MCP

CI License: MIT Node

An MCP server that lets an AI agent compose chiptune music in Lovely Composer on your own machine.

It is a zero-dependency Node.js server speaking the stdio transport. It works by reading and writing Lovely Composer's .jsonl project files, so an agent can create songs, write melodies / bass / drums / chords, change tempo and loop points — and you just open the folder in Lovely Composer to listen to it, edit it, or export it.

中文说明见 README.zh-CN.md

Why this approach

Driving a GUI is brittle: window focus, timing, and whatever else you are doing on the computer all get in the way. Lovely Composer stores a song as a plain two-line JSON file, so this server edits the data directly. That means:

  • no GUI automation, no window focus, no interference with your desktop;

  • the agent gets a precise, inspectable data model instead of pixels;

  • the files it writes are the exact files Lovely Composer itself writes — see Verification for how that is proven.

Related MCP server: OpenUtau-MCP

Requirements

  • Node.js 18 or newer (for TextDecoder with CJK codecs).

  • Lovely Composer installed. It is auto-detected in the usual Steam locations and in Documents\LovelyComposer; override with LC_MUSIC_ROOT.

Install

Clone it anywhere and point your MCP client at src/server.js.

git clone https://github.com/baichuan4167-lang/lovely-composer-mcp.git
cd lovely-composer-mcp
node test/smoke.js     # optional: prove it works on your machine

There is nothing to npm install — the server has no dependencies.

Generic MCP client configuration

Most clients take the same stdio server block:

{
  "mcpServers": {
    "lovelycomposer": {
      "command": "node",
      "args": ["/absolute/path/to/lovely-composer-mcp/src/server.js"]
    }
  }
}

For Claude Desktop that goes in claude_desktop_config.json. Some hosts add a cwd and a failOnStartupError flag; both are harmless here.

Environment variables

Variable

Used by

Purpose

LC_MUSIC_ROOT

server

Force the music library root instead of auto-detecting it.

LC_MCP_PROTOCOL_VERSION

server

Pin the negotiated MCP protocol revision (default 2025-06-18).

LC_SAMPLE_PATH

test/smoke.js

Path to a real .jsonl song for the integration checks.

LC_APP_DIR

tools/*.py

Path to <LovelyComposer>/app, for the validators below.

Then just talk to your agent:

Use Lovely Composer to write me a 16-bar 8-bit loop in A minor with a melody, a bass line and drums.

Read song 00 in the DSH folder and raise page 2's melody by an octave.

Tools

Tool

What it does

lc_status

Reports the install path, music root, writability and the data model. Call this first.

lc_list_folders

Lists the song folders in the music library.

lc_list_songs

Lists songs in a folder (title / speed / pages / notes per channel).

lc_read_song

Reads a song and prints each channel as compact per-page patterns.

lc_create_song

Creates an empty song (and the lcdata.jsonl a new folder needs).

lc_write_page

The main composing tool. Writes one page of one channel from a pattern string.

lc_set_notes

Places notes at exact (channel, page, tick), including chord-track chords.

lc_clear_pages

Erases the given channels and pages.

lc_set_song_options

Title, author, speed, page count, ticks per page, loop points, scale.

lc_list_instruments

Every instrument preset, effect letter and scale.

lc_copy_song

Copies a song (handy to start from an existing tune).

lc_delete_song

Deletes a song file; requires force=true.

Pattern string syntax

lc_write_page takes one whitespace-separated token per tick, starting at tick 0, up to 32 ticks per page:

C5@25 . . E5 . G5@25 . . - . . .     C5 with preset 25; "." empty; "-" hold

Token

Meaning

. / R

empty tick

-

hold: extend the previous note

< / >

hold with fade-in / fade-out

C4

note using the page's default instrument

C4@25

note with instrument preset 25

C4@flute

instrument by name

C4@16*7+D^A~8%3

plus volume *0-7, effect +, expression ^0-F, pan ~0-F, envelope %0-F

C4:S4NC00::2

a raw Lovely Composer voice string, used verbatim

Common presets (full list via lc_list_instruments): 0 pulse · 1 triangle · 2 square · 3 noise · 4 piano · 7 drum · 16 sawtooth · 25 flute · 30 stomp (kick) · 33 punch (snare) · 35 short-freq noise (hat) · 47 low-reso triangle (bass) · 56 bell · 68 fast arpeggio.

Effect letters: N none · S slur/slide · V vibrato · F fade out · I fade in · D drop · H hop · A fast arpeggio · P phaser, and more.

Data model

A song is one folder entry, <music>/<FOLDER>/<NN>.jsonl (NN = 00..99):

  • 5 channels: 0–3 melodic, 4 = the chord track.

  • Each song has pages pages, each page up to 32 ticks.

  • Every page carries its own length (ticksPerPage) and speed.

  • Default barsPerPage=4: 4 bars per page, 8 ticks per bar.

  • Scientific pitch notation: C4 is middle C, range A0–C8.

  • Tempo: bpm = 900 × barsPerPage / speed, so lower speed means a faster song. speed=30, bars=4 gives 120 BPM.

Chord-track notes use Lovely Composer's chord encoding: id = ((type + velocity<<4 + seventh<<6 + ninth<<8) << 16) + 65536, where type is 1=major, 2=minor, 3=sus4, 4=aug, 5=dim. With lc_set_notes you just write chord: "minor".

⚠️ Lovely Composer rewrites the whole folder on save

This matters more than anything else in this README:

  • Lovely Composer reads the entire folder when it opens it and rewrites all 100 song files when it saves. Reload the folder (or restart LC) to see what the agent wrote.

  • If LC currently has the folder open, do not press save in LC before reloading, or it will overwrite what the agent just wrote.

  • Songs LC marks as write-protected (write_protected_flag; all the bundled sample songs are) are refused by default. Pass force=true to override.

The file format

The serialization is a straight obj.__dict__ dump tagged with the class name:

{"__LCVoice__": true, "n": 60, "t": 1, "v": 4, "f": 0, "id": 2, "x": 12, "p": 0, "e": 0}

n pitch (null empty, -1 rest) · t oscillator · v volume 0-7 · f effect · id instrument preset · x expression · p pan · e envelope.

The __LCVoice__ tag has to be the first key. LC's json_loader_hook walks the dict and rebuilds a real object from the first key that names a class it knows; without the tag the voice degrades to a plain dict and LC then breaks on attribute access.

A song file is exactly two lines (CRLF-separated, no trailing newline after the second — matching what LC writes):

{"__LCMusicDataHeader__": true, ...}
{"__LCMusic__": true, "speed":…, "channels":{…}, "rhythms":{…}, …}

LC builds a default instance and merges the loaded dict over it, so omitted keys keep LC's own defaults. This implementation writes only the keys it actually needs and lets LC supply wave memory, sampling modulators and the rest.

Container hierarchy: LCMusic → LCChannelList.channels[5] → LCSoundList.sl[pages] → LCSound.vl[32] → LCVoice.

⚠️ Encoding: LC reads files with the system ANSI code page, not UTF-8

LC reads and writes project files with Python's open(path) and no explicit encoding, so it uses the locale default — GBK/cp936 on a Chinese Windows, cp932 on a Japanese one.

A UTF-8 file with a non-ASCII title makes LC raise UnicodeDecodeError, so load_music() fails and the song simply will not open. The trap is subtle because LC's own sample songs have pure-ASCII titles.

How this project handles it:

  • Writing — every non-ASCII character is escaped to \uXXXX, so the file is pure ASCII on disk. Pure ASCII decodes identically under GBK and UTF-8, so it works on any locale.

  • Reading — try strict UTF-8 first, then fall back through GB18030 / GBK / Big5 / Shift_JIS, taking the first result that JSON.parse accepts (LC stores non-ASCII as raw GBK bytes when it saves).

Tempo and grid: barsPerPage drives both BPM and resolution

bpm = 900 × barsPerPage / speed, and a page always plays ticksPerPage ticks, so changing barsPerPage changes both the tempo and the editable resolution:

barsPerPage

ticksPerPage

ticks per bar

finest note

for 180 BPM use

4 (default)

32

8

eighth note

speed 30 → 120 BPM

2

32

16

sixteenth note

speed 10

1

32

32

thirty-second note

speed 5

For J-core / denpa sixteenth-note runs, use barsPerPage=1, ticksPerPage=32: one page is then one bar, and 1 tick = a 32nd note.

Instrument preset parameters come from the game's app/lcl/common.py (VOICE_STR_LIST, after LC's own expression/pan post-processing) and were checked entry by entry against real project files.

Verification

The claim "the files it writes are real Lovely Composer files" is tested with the game's own code, not just with this project's parser:

$py = "<LovelyComposer>\app\python\python.exe"

# Full validation: model types, containers, and an LCJSONEncoder round trip
& $py -u tools/validate_with_lc.py "<music>\DSH\01.jsonl"

# Replay lcl.load_music() stage by stage, next to an LC sample song
& $py -u tools/diagnose_load.py "<music>\DSH\01.jsonl"

With LC_APP_DIR unset these search the usual Steam locations; set it to <LovelyComposer>\app if your install lives somewhere unusual.

diagnose_load.py prints every stage of LC opening a song. A generated song reaches exactly the same stages as an LC sample:

[1] read+parse            OK  header=LCMusicDataHeader music=LCMusic title='…'
[2] trim channels         OK  5 remain
[3] version check         OK  16 <= 16
[4] _old_lcmusic_data_updater  OK
[5] model usable          OK  pages=48 speed=5 bpm=181 notes=2158
[6] set_wave_memory_from_lcmusic_settings ... (doxel audio layer, needs a writable game dir)

Stage 6 aborts the process: doxel insists on writing its log inside the game install directory and calls os._exit() when that is denied, e.g. under a restricted sandbox. That is unrelated to the data format — LC's own sample songs stop at the same point in the same environment.

Included demos

Two songs were written with this server and live in the DSH folder of the author's library:

#

Title

Notes

00

Neon Loop

8-bar beginner loop, A minor, melody / bass / drums / chords.

01

配信中毒 - STREAM OVERDOSE

48-bar piece, 180 BPM, F# minor, denpa / J-core, 2158 notes.

compositions/stream-overdose.js regenerates the second one.

Development

node test/smoke.js            # 36 self-checks: round trips, encoding fallback, DSL
node test/demo-song.js        # compose a demo song into .tmp-test/DEMO
node test/demo-song.js DSH 1  # write straight into a real library folder
node src/server.js < test/protocol-probe.jsonl   # exercise the JSON-RPC layer

The self-test covers note-name conversion, the instrument table, chord encoding, the pattern DSL, full song round trips, write protection and the GBK fallback. When a real Lovely Composer install is present it also round-trips a song the game wrote — and silently skips that part when there is none, so CI stays green.

Files

File

Purpose

src/lc.js

Format engine: constants, note/instrument/chord conversion, pattern parsing, project file I/O and encoding.

src/tools.js

The 12 MCP tool definitions and their implementations.

src/server.js

stdio JSON-RPC MCP server (zero dependencies, hand-written protocol layer).

compositions/stream-overdose.js

Full composition generator (denpa / J-core, 48 bars, 2158 notes).

test/smoke.js

Self-test suite.

test/demo-song.js

Demo composer that goes through the tool handlers.

test/protocol-probe.jsonl

JSON-RPC probe requests.

tools/validate_with_lc.py

Validates a file with the game's own lcl module.

tools/diagnose_load.py

Replays LC's load_music() step by step.

tools/lcenv.py

Finds the game install so the validators can import lcl.

tools/check-encoding.js

Flags song files containing non-ASCII bytes.

tools/analyze_lc.py

Structural analyzer used during reverse engineering.

Known limitations

  • It can only write project files; it cannot trigger an export. Export to WAV/MIDI from Lovely Composer itself, or set up an LC addon to do it.

  • It does not drive LC's GUI, so it never depends on window focus.

  • Wave memory and sampling modulator parameters are left to LC's defaults and are not editable here.

  • The rhythm track (rhythms) uses LC's new-song defaults; there is no editing interface for it yet.

License

MIT

Available Tools

12 tools
lc_clear_pagesC

Erase every note in the given channels and pages (defaults to all of them).

Inputs: song (required), folder (required), force, pages, +1 more.

lc_copy_songA

Copy a song to another folder/slot (useful to start from an existing tune). Clears write protection.

Inputs: song (required), folder (required), toSong (required), toFolder (required), +2 more.

lc_create_songA

Create a new empty song file at //.jsonl. Fails if the song already exists unless force=true. Returns the usable tick range and an approximate BPM.

Inputs: song (required), folder (required), force, pages, +10 more.

lc_delete_songA

Delete a song file. Requires force=true; there is no undo.

Inputs: song (required), folder (required), force.

lc_list_foldersA

List the song folders inside the Lovely Composer music root, with song counts and a few titles.

Inputs: no named parameters.

lc_list_instrumentsA

List every instrument preset (id + name), the note modifier syntax, the effect letters, and the available scales. Call this before composing so you pick real presets.

Inputs: no named parameters.

lc_list_songsB

List the songs (00..99) in one folder with title, speed, page count and note counts per channel.

Inputs: folder (required).

lc_read_songA

Read a song and print its settings plus each channel as compact per-page patterns ("C5@2 . . E5@2 ..."), so you can inspect and edit existing music.

Inputs: song (required), folder (required), pages, channels, +1 more.

lc_set_notesA

Place individual notes at exact (channel, page, tick) positions — use when a pattern string is awkward. Each event needs channel/page/tick plus note and either instrument (preset id or name) or chord (major/minor/sus4/aug/dim, chord track only). Optional volume/effect/expression/pan/envelope, or clear=true to empty that tick. Events that fail are reported and skipped.

Inputs: song (required), notes (required), folder (required), force.

lc_set_song_optionsC

Change song-level settings: title, editor, speed, page count, ticks per page, loop points, scale.

Inputs: song (required), folder (required), force, pages, +10 more.

lc_statusA

Report the detected Lovely Composer installation, music folder root, whether it is writable, and the song data model (channels, pages, ticks, note range). Call this first.

Inputs: no named parameters.

lc_write_pageA

Write one page of one channel from a compact pattern string — the main composing tool. One whitespace-separated token per tick starting at tick 0: "." or "R" = empty, "-" = hold the previous note, "<"/">" = hold with fade-in/fade-out, "C5" = note, "C5@25" = note with instrument preset 25. Optional modifiers: *volume(0-7) +effect(letter, e.g. +S slur) ^expression(0-F) ~pan(0-F) %envelope(0-F). Example: "C5@25 . . E5 . G5@25 . . - . . ." The page is replaced entirely.

Inputs: page (required), song (required), folder (required), channel (required), +4 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 12 tools

    Disambiguation5/5

    Each tool has a clear, distinct purpose: status, listings, read/create/write/set/clear/copy/delete operations are all separated by resource and action granularity. The closest pair, lc_write_page and lc_set_notes, is distinguished by bulk page replacement versus individual note placement, eliminating confusion.

    Naming Consistency4/5

    All tools share the lc_ prefix and follow a verb_noun snake_case pattern (list_folders, read_song, create_song, etc.). The only deviation is lc_status, which uses a noun instead of a verb such as get_status, but the overall pattern remains highly predictable.

    Tool Count5/5

    With 12 tools, the set is well-scoped for the domain: it covers environment inspection, navigation, song lifecycle, page/note editing, options, and instrument reference without redundancy or bloat. Each tool earns its place in a typical composition workflow.

    Completeness4/5

    The tool surface covers the full song lifecycle (create, read, edit, copy, delete), plus detailed editing (page patterns, individual notes, clearing) and reference data (instruments, scales). Minor gaps like a dedicated rename tool or an undo operation are non-critical and workaroundable via copy/delete.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      A game-agnostic MCP toolkit for composing chiptune/retro MIDI and rendering game audio, enabling AI agents to generate multi-track MIDI files and render them to OGG/WAV.
      7
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to generate, inspect, and micro-tune OpenUtau .ustx project files and DiffSinger neural expression curves.
      5
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Enables AI agents to create, modify, and save LMMS music projects programmatically, including tracks, notes, mixer control, effects, and song settings.
      4
      52
      3
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      ismail is a DAW your agent drives through MCP, CLI or Python. The agent writes notes, instruments, effects and automation as data. ismail's own engine renders the audio, and every render comes back as text the agent can read. No AI model generates the audio. It runs on your own machine. It is free and open source. https://github.com/newsbubbles/ismail
      373
      329 PyPI
      18
      MIT