summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-11 21:48:34 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-11 21:48:34 +0900
commit4ed98f4ec8d354f8fdb009f0b7829c2628adb2a6 (patch)
tree07fd47ae951fa3ad776834e4876764adb09d5068
parent79cd969c98893e04382fe6075f420589c3a1652e (diff)
fix: stdio transport 起動時の TypeError 修正とトラック外コマンド警告の追加
-rw-r--r--README.md364
-rw-r--r--src/mml_composemusic_mcp/ir.py1
-rw-r--r--src/mml_composemusic_mcp/parser_ppmck.py14
-rw-r--r--src/mml_composemusic_mcp/parser_pyxel.py14
-rw-r--r--src/mml_composemusic_mcp/server.py9
-rw-r--r--tests/test_error_codes.py13
-rw-r--r--tests/test_transports.py12
7 files changed, 394 insertions, 33 deletions
diff --git a/README.md b/README.md
index 351c9c9..231c56e 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,359 @@
# mml-composemusic-mcp
-ファミコン音源(2A03 APU)風のMMLをLLMに作曲させるためのMCPサーバ。
+ファミコン音源(2A03 APU)風のMMLをLLMに作曲させるためのMCPサーバです。
-## 機能
+`compose_mml` という1つのツールで、MMLの**作曲(WAV生成)**、**構文検証**、**テンプレート生成**を行えます。
-- `compose_mml` ツールを提供
-- ppmck 形式と pyxel 形式のMMLを解析・合成
-- MIDIノート番号に変換後、NES APU風の矩形波/三角波/ノイズでWAV出力
-- バリデーション、テンプレート生成に対応
+---
-## ツール引数
+## 目次
-| 引数 | 説明 |
-|------|------|
-| `action` | `compose`, `validate`, `template` のいずれか |
-| `mml` | MML文字列(compose/validate時必須) |
-| `mode` | `ppmck` または `pyxel`(compose/validate時必須) |
-| `template` | `basic`, `melody`, `chord`, `drum`, `empty`(action=template時) |
-| `sample_rate` | WAV出力サンプリングレート(デフォルト44100) |
-| `normalize` | 出力振幅の正規化(デフォルトtrue) |
+1. [機能概要](#機能概要)
+2. [動作環境](#動作環境)
+3. [インストール](#インストール)
+4. [MCPクライアントへの登録](#mcpクライアントへの登録)
+5. [ツールの使い方](#ツールの使い方)
+6. [MML形式](#mml形式)
+7. [テスト・開発](#テスト開発)
+8. [ディレクトリ構成](#ディレクトリ構成)
+9. [ライセンス](#ライセンス)
-## 実行
+---
+
+## 機能概要
+
+| 機能 | 説明 |
+|---|---|
+| `compose` | MMLを解析し、NES APU風の矩形波/三角波/ノイズでWAVファイルを生成する |
+| `validate` | MMLの構文を検証し、エラー・警告・チャンネル概要を返す |
+| `template` | `ppmck` / `pyxel` 用のテンプレートMMLを生成する |
+
+### 対応MML形式
+
+| モード | 特徴 |
+|---|---|
+| `ppmck` | PPMCKインスパイアのFC準拠形式。小文字コマンド、`A`/`B`/`T`/`N` トラック |
+| `pyxel` | Pyxel MML準拠形式。大文字コマンド、`0:`〜`3:` トラック、リピート・ゲートタイム対応 |
+
+---
+
+## 動作環境
+
+- Python 3.14 以上
+- [uv](https://docs.astral.sh/uv/)(推奨)
+- 依存パッケージ: `fastmcp`, `numpy`
+
+---
+
+## インストール
+
+### 1. リポジトリをクローンまたは配置
```bash
-uv run mml-composemusic-mcp --output-dir ./data
+cd C:\\path\\to\\mml-composemusic-mcp
```
-または
+### 2. 依存関係をインストール
+
+`uv` を使う場合:
```bash
-uv run python -m mml_composemusic_mcp.server --output-dir ./data
+uv sync
```
-## テスト
+`pip` を使う場合:
+
+```bash
+pip install -e .
+```
+
+### 3. 起動確認
+
+```bash
+uv run mml-composemusic-mcp --help
+```
+
+以下のように表示されればOKです。
+
+```
+usage: mml-composemusic-mcp [-h] [--output-dir OUTPUT_DIR]
+ [--transport {stdio,http,sse,streamable-http}] [--host HOST] [--port PORT]
+```
+
+---
+
+## MCPクライアントへの登録
+
+### Claude Desktop の場合
+
+1. 設定ファイルを開きます。
+
+ | OS | パス |
+ |---|---|
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
+
+2. `mcpServers` に以下を追加します。
+
+ ```json
+ {
+ "mcpServers": {
+ "mml-composemusic": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "C:\\path\\to\\mml-composemusic-mcp",
+ "mml-composemusic-mcp",
+ "--output-dir",
+ "C:\\path\\to\\mml-composemusic-mcp\\data"
+ ]
+ }
+ }
+ }
+ ```
+
+3. Claude Desktop を再起動します。
+
+### 設定ファイル例
+
+[`doc/mcp-client-config-example.json`](doc/mcp-client-config-example.json) に設定例があります。必要に応じてコピーしてください。
+
+### トランスポートの選択
+
+| トランスポート | 用途 | 設定方法 |
+|---|---|---|
+| `stdio` | 標準的なMCPクライアント接続(デフォルト) | `command`/`args` で指定 |
+| `http` | HTTPエンドポイント | `--transport http` で起動後、`url` で接続 |
+| `sse` | Server-Sent Events | `--transport sse` で起動後、`url` で接続 |
+| `streamable-http` | Streamable HTTP | `--transport streamable-http` で起動 |
+
+#### SSEで使う場合の例
+
+```bash
+uv run mml-composemusic-mcp --transport sse --port 8080 --output-dir ./data
+```
+
+```json
+{
+ "mcpServers": {
+ "mml-composemusic": {
+ "url": "http://127.0.0.1:8080/sse"
+ }
+ }
+}
+```
+
+### 注意点
+
+- `--output-dir` は相対パスでも動きますが、MCPクライアントの作業ディレクトリが不定なため、**絶対パスを推奨**します。
+- 初回起動時は `uv` が依存関係を解決するため、少し時間がかかることがあります。
+
+---
+
+## ツールの使い方
+
+### ツール名
+
+`compose_mml`
+
+### 引数
+
+| 引数名 | 型 | 必須 | デフォルト | 説明 |
+|---|---|---|---|---|
+| `action` | string | yes | — | `compose`, `validate`, `template` のいずれか |
+| `mml` | string | compose/validate時 | `""` | MMLソース文字列 |
+| `mode` | string | compose/validate時 | `""` | `ppmck` または `pyxel` |
+| `template` | string | template時 | `"basic"` | `basic`, `melody`, `chord`, `drum`, `empty` |
+| `sample_rate` | integer | no | `44100` | 出力WAVのサンプリングレート(Hz) |
+| `normalize` | boolean | no | `true` | 出力振幅を正規化するか |
+
+### action=`compose` — WAVを生成
+
+```json
+{
+ "action": "compose",
+ "mml": "0: T120 L8 O4 V100 @1\n C D E F | G A B >C",
+ "mode": "pyxel",
+ "sample_rate": 44100,
+ "normalize": true
+}
+```
+
+#### 戻り値
+
+```json
+{
+ "success": true,
+ "wav_path": "./data/output.wav",
+ "duration_sec": 2.0,
+ "note_sequence": { ... },
+ "validation": {
+ "errors": [],
+ "warnings": []
+ }
+}
+```
+
+- `success` が `false` の場合、`wav_path` は `null` になります。
+- エラーがある場合は `validation.errors` に詳細なエラー情報が入ります。
+
+### action=`validate` — 構文チェック
+
+```json
+{
+ "action": "validate",
+ "mml": "A t120 l8 o4 v15 q2\n c d e f",
+ "mode": "ppmck"
+}
+```
+
+#### 戻り値
+
+```json
+{
+ "valid": true,
+ "errors": [],
+ "warnings": [],
+ "note_sequence": { ... },
+ "channel_summary": [
+ {
+ "channel": "Pulse1",
+ "note_count": 4,
+ "octave_range": [4, 5],
+ "duration_ticks": 768
+ }
+ ]
+}
+```
+
+### action=`template` — テンプレート生成
+
+```json
+{
+ "action": "template",
+ "mode": "pyxel",
+ "template": "basic"
+}
+```
+
+#### 戻り値
+
+```json
+{
+ "mml": "0: T120 L8 O4 V100 @1\n C D E F | ...",
+ "description": "基本的な4ch構成(メロディ+和音+ベース+リズム)"
+}
+```
+
+### テンプレート種別
+
+| テンプレート | 内容 |
+|---|---|
+| `basic` | 基本的な4ch構成(メロディ+和音+ベース+リズム) |
+| `melody` | メロディ重視(Pulse1主旋律、他は伴奏最小限) |
+| `chord` | コード伴奏重視(Pulse2で和音、Triangleでベース) |
+| `drum` | リズム重視(Noise中心のビートパターン) |
+| `empty` | 各チャンネルのヘッダーのみ(空のテンプレート) |
+
+---
+
+## MML形式
+
+### ppmck 形式の例
+
+```
+#TITLE "My Song"
+#COMPOSER "LLM"
+
+A t150 l8 o4 v15 q2
+ c d e f | g a b > c
+
+B l8 o3 v12 q1
+ c r g r c r g r
+
+T l4 o3 v7
+ c2 c2 g2 g2
+
+N l8 v10
+ r c r c r c r c
+```
+
+### pyxel 形式の例
+
+```
+0: T150 L8 O4 V100 @1
+ C D E F G A B >C
+
+1: L8 O3 V80 @2
+ E G B R E G B R
+
+2: L4 O3 V60
+ C2 G2 E2 C2
+
+3: L8 V80
+ C R C R C R C R
+```
+
+### 詳細仕様
+
+詳細なMMLコマンド仕様、IR構造、エラーコードは [`doc/Design.md`](doc/Design.md) を参照してください。
+
+---
+
+## テスト・開発
+
+### テスト実行
```bash
uv run pytest
+```
+
+### リント・フォーマット
+
+```bash
uv run ruff check .
uv run ruff format .
```
+### 手動でサーバを起動
+
+```bash
+uv run mml-composemusic-mcp --output-dir ./data
+```
+
+または:
+
+```bash
+uv run python -m mml_composemusic_mcp.server --output-dir ./data
+```
+
+---
+
## ディレクトリ構成
-- `src/mml_composemusic_mcp/`
- - `server.py` - MCPサーバ
- - `lexer.py` - MML字句解析
- - `parser_ppmck.py`, `parser_pyxel.py` - 構文解析
- - `ir.py` - 中間表現・エラー型
- - `synthesizer.py` - APU風合成・WAV出力
- - `templates.py` - テンプレート
-- `tests/` - テスト
-- `doc/` - 設計書
+```
+.
+├── doc/ # 設計書・設定例
+│ ├── Design.md # 統合設計書
+│ ├── mcp.md # MCPツールスキーマ
+│ └── mcp-client-config-example.json # MCPクライアント設定例
+├── src/mml_composemusic_mcp/ # ソースコード
+│ ├── server.py # MCPサーバ
+│ ├── lexer.py # MML字句解析
+│ ├── parser_ppmck.py # ppmckパーサ
+│ ├── parser_pyxel.py # pyxelパーサ
+│ ├── parser_base.py # パーサ共通処理
+│ ├── ir.py # 中間表現・エラー型
+│ ├── synthesizer.py # APU風合成・WAV出力
+│ └── templates.py # テンプレート
+├── tests/ # テスト
+├── README.md # このファイル
+└── pyproject.toml # プロジェクト設定
+```
+
+---
+
+## ライセンス
+
+(プロジェクトのライセンスをここに記載してください)
diff --git a/src/mml_composemusic_mcp/ir.py b/src/mml_composemusic_mcp/ir.py
index 42adc13..bd741a6 100644
--- a/src/mml_composemusic_mcp/ir.py
+++ b/src/mml_composemusic_mcp/ir.py
@@ -20,6 +20,7 @@ class ErrorCode(Enum):
SYNTAX_EMPTY_TRACK = "SYNTAX_EMPTY_TRACK"
SYNTAX_NOTE_OUT_OF_RANGE = "SYNTAX_NOTE_OUT_OF_RANGE"
SYNTAX_CHANNEL_MISMATCH = "SYNTAX_CHANNEL_MISMATCH"
+ SYNTAX_OUTSIDE_TRACK = "SYNTAX_OUTSIDE_TRACK"
SYNTAX_UNTERMINATED_HEADER = "SYNTAX_UNTERMINATED_HEADER"
SYNTAX_UNDEFINED_REFERENCE = "SYNTAX_UNDEFINED_REFERENCE"
diff --git a/src/mml_composemusic_mcp/parser_ppmck.py b/src/mml_composemusic_mcp/parser_ppmck.py
index db91950..67e5093 100644
--- a/src/mml_composemusic_mcp/parser_ppmck.py
+++ b/src/mml_composemusic_mcp/parser_ppmck.py
@@ -62,6 +62,7 @@ class PpmckParser:
self._parse_track_header()
continue
if self.current_channel is None:
+ self._warn_outside_track(token)
self.ctx.advance()
continue
self._parse_statement()
@@ -109,6 +110,19 @@ class PpmckParser:
)
self.headers.append((key, value))
+ def _warn_outside_track(self, token: Token) -> None:
+ if token.type in (TokenType.EOF, TokenType.COMMENT):
+ return
+ self.ctx.add_error(
+ code=ErrorCode.SYNTAX_OUTSIDE_TRACK,
+ line=token.line,
+ column=token.column,
+ message=f"'{token.raw}' はトラック外に書かれています。",
+ severity="warning",
+ hint="コマンドはトラックヘッダー(A, B, T, N)の後に記述してください。",
+ context=self._context_line(token),
+ )
+
def _parse_track_header(self) -> None:
token = self.ctx.advance()
ch = token.value.upper()
diff --git a/src/mml_composemusic_mcp/parser_pyxel.py b/src/mml_composemusic_mcp/parser_pyxel.py
index 336a55e..f75c169 100644
--- a/src/mml_composemusic_mcp/parser_pyxel.py
+++ b/src/mml_composemusic_mcp/parser_pyxel.py
@@ -76,6 +76,7 @@ class PyxelParser:
self._parse_track_header()
continue
if self.current_channel is None:
+ self._warn_outside_track(token)
self.ctx.advance()
continue
self._parse_statement()
@@ -102,6 +103,19 @@ class PyxelParser:
return self.note_sequence, self.ctx.errors
+ def _warn_outside_track(self, token: Token) -> None:
+ if token.type == TokenType.EOF:
+ return
+ self.ctx.add_error(
+ code=ErrorCode.SYNTAX_OUTSIDE_TRACK,
+ line=token.line,
+ column=token.column,
+ message=f"'{token.raw}' はトラック外に書かれています。",
+ severity="warning",
+ hint="コマンドはトラックヘッダー(0:, 1:, 2:, 3:)の後に記述してください。",
+ context=self._context_line(token),
+ )
+
def _parse_track_header(self) -> None:
token = self.ctx.advance()
ch = token.value
diff --git a/src/mml_composemusic_mcp/server.py b/src/mml_composemusic_mcp/server.py
index 3061e6d..f21159a 100644
--- a/src/mml_composemusic_mcp/server.py
+++ b/src/mml_composemusic_mcp/server.py
@@ -274,7 +274,14 @@ def main() -> None:
args = parser.parse_args()
global OUTPUT_DIR
OUTPUT_DIR = Path(args.output_dir)
- mcp.run(transport=args.transport, host=args.host, port=args.port)
+ if args.transport == "stdio":
+ mcp.run(transport="stdio")
+ else:
+ mcp.run(
+ transport=args.transport,
+ host=args.host,
+ port=args.port,
+ )
if __name__ == "__main__":
diff --git a/tests/test_error_codes.py b/tests/test_error_codes.py
index 496ce84..eca73ad 100644
--- a/tests/test_error_codes.py
+++ b/tests/test_error_codes.py
@@ -82,6 +82,19 @@ def test_syntax_unexpected_token(compose_mml):
assert result["valid"] is True
+# --- SYNTAX_OUTSIDE_TRACK ---
+def test_syntax_outside_track_ppmck(compose_mml):
+ result = compose_mml(action="validate", mml="t120\nA l4\n c", mode="ppmck")
+ assert result["valid"] is True
+ assert _find_code(result, ErrorCode.SYNTAX_OUTSIDE_TRACK.value, "warnings")
+
+
+def test_syntax_outside_track_pyxel(compose_mml):
+ result = compose_mml(action="validate", mml="T120\n0: L4\n C", mode="pyxel")
+ assert result["valid"] is True
+ assert _find_code(result, ErrorCode.SYNTAX_OUTSIDE_TRACK.value, "warnings")
+
+
# --- SYNTAX_UNTERMINATED_REPEAT ---
def test_syntax_unterminated_repeat_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4\n[ C D", mode="pyxel")
diff --git a/tests/test_transports.py b/tests/test_transports.py
index 395977b..a8bf12f 100644
--- a/tests/test_transports.py
+++ b/tests/test_transports.py
@@ -8,13 +8,15 @@ from mml_composemusic_mcp.server import main
def test_transport_stdio_default():
- """Default transport should be stdio."""
+ """Default transport should be stdio without host/port."""
with patch("sys.argv", ["server", "--output-dir", "./data"]):
with patch("mml_composemusic_mcp.server.mcp.run") as mock_run:
main()
mock_run.assert_called_once()
call_kwargs = mock_run.call_args
assert call_kwargs[1]["transport"] == "stdio"
+ assert "host" not in call_kwargs[1]
+ assert "port" not in call_kwargs[1]
def test_transport_http():
@@ -32,21 +34,25 @@ def test_transport_http():
def test_transport_sse():
- """SSE transport should be accepted."""
+ """SSE transport should pass host and port."""
with patch("sys.argv", ["server", "--transport", "sse", "--port", "8080"]):
with patch("mml_composemusic_mcp.server.mcp.run") as mock_run:
main()
call_kwargs = mock_run.call_args
assert call_kwargs[1]["transport"] == "sse"
+ assert "host" in call_kwargs[1]
+ assert call_kwargs[1]["port"] == 8080
def test_transport_streamable_http():
- """Streamable HTTP transport should be accepted."""
+ """Streamable HTTP transport should pass host and port."""
with patch("sys.argv", ["server", "--transport", "streamable-http"]):
with patch("mml_composemusic_mcp.server.mcp.run") as mock_run:
main()
call_kwargs = mock_run.call_args
assert call_kwargs[1]["transport"] == "streamable-http"
+ assert "host" in call_kwargs[1]
+ assert "port" in call_kwargs[1]
def test_transport_invalid_rejected():