@hidechae/eztab-mcp 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/server.js +81 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
102
102
|
| `add_measures` / `remove_measure` | Insert/remove measures (keys & sections re-anchored) |
|
|
103
103
|
| `set_tuning` | Replace tuning / string count (`track` で対象パートを指定) |
|
|
104
104
|
| `add_track` / `remove_track` | パート(ベース・2本目のギターなど)の追加・削除。メインを含めて最大4、track 0(メイン)は削除不可 |
|
|
105
|
+
| `set_tone` | パートの再生音色を設定する(`track` で対象パートを指定)。`add_track` でも `tone` を指定できる |
|
|
105
106
|
|
|
106
107
|
### 複数パート(トラック)
|
|
107
108
|
|
|
@@ -113,10 +114,21 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
113
114
|
- コードは track 0 のもの(`set_chords` は track 0 の列で指定)。`write_measures(track > 0)` ではコード指定と、
|
|
114
115
|
既存小節の拍子を変える `time` を拒否する
|
|
115
116
|
- `get_tab` は全パートを小節ごとに並べて表示する(`track` で1パートに絞れる)
|
|
116
|
-
- `send_to_eztab` は対応する最新の形式リビジョンをヘッダーに付けて送る(0.
|
|
117
|
+
- `send_to_eztab` は対応する最新の形式リビジョンをヘッダーに付けて送る(0.6.0では `7`)。
|
|
117
118
|
対応上限を超える形式のタブは閲覧のみで、編集・送信できない。
|
|
118
119
|
0.2.0以前のMCPは複数パートのタブを送り返せない(サーバが409で拒否する)。
|
|
119
120
|
|
|
121
|
+
### パートの音色
|
|
122
|
+
|
|
123
|
+
0.6.0以降はパートごとの再生音色(形式リビジョン7、docs/design/0010-track-tones.md)を扱えます。
|
|
124
|
+
|
|
125
|
+
- 値は `clean`(既定)/ `crunch` / `distortion` / `acoustic` / `piano` / `synth` / `strings`
|
|
126
|
+
- 値の意味はパートの楽器で変わる。名前かチューニングでベースと判定されるパートでは、
|
|
127
|
+
`crunch` はオーバードライブ、`acoustic` はウッドベース、`synth` はシンセベースとして鳴る
|
|
128
|
+
- `get_tab` はクリーン以外の音色をパート一覧に `[distortion]` のように表示する
|
|
129
|
+
(1パートのタブでは `tone=distortion`)
|
|
130
|
+
- 0.5.2以前のMCPは、クリーン以外の音色を持つタブを編集・送信できない
|
|
131
|
+
|
|
120
132
|
### 小節メモの保持
|
|
121
133
|
|
|
122
134
|
0.5.1以降はアプリで入力した小節共通メモ(形式リビジョン5)を保持して編集・送信できます。
|
|
@@ -146,3 +158,5 @@ Claude Desktop では入力欄の「+」→ eztab → `import_tab_from_image`
|
|
|
146
158
|
EZTab のモデルに無い記譜(スライド・ハンマリング・プリング → タイ/スラー、
|
|
147
159
|
ハーモニクス・ビブラート・装飾音 → 主音のみ等)の寄せ方は手順書
|
|
148
160
|
([prompts.ts](prompts.ts))に書いてあり、書き起こし後に簡略化した箇所を報告させる。
|
|
161
|
+
|
|
162
|
+
0.5.2以降は曲全体のMarkdownメモ(形式リビジョン6)を既存の編集・送信操作で保持します。曲メモの入力・表示はwebアプリで行います。
|
package/dist/server.js
CHANGED
|
@@ -7,12 +7,26 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
// ../src/types/tab.ts
|
|
9
9
|
var TAB_FILE_VERSION = 1;
|
|
10
|
-
var SUPPORTED_FORMAT_REVISION =
|
|
10
|
+
var SUPPORTED_FORMAT_REVISION = 7;
|
|
11
|
+
var SONG_MEMO_FORMAT_REVISION = 6;
|
|
11
12
|
var MULTI_TRACK_FORMAT_REVISION = 2;
|
|
12
13
|
var HARMONICS_FORMAT_REVISION = 3;
|
|
13
14
|
var TEMPO_CHANGES_FORMAT_REVISION = 4;
|
|
14
15
|
var MEASURE_MEMOS_FORMAT_REVISION = 5;
|
|
16
|
+
var TONE_FORMAT_REVISION = 7;
|
|
15
17
|
var MAX_TRACKS = 4;
|
|
18
|
+
var TONES = [
|
|
19
|
+
"clean",
|
|
20
|
+
"crunch",
|
|
21
|
+
"distortion",
|
|
22
|
+
"acoustic",
|
|
23
|
+
"piano",
|
|
24
|
+
"synth",
|
|
25
|
+
"strings"
|
|
26
|
+
];
|
|
27
|
+
function normalizeTone(raw) {
|
|
28
|
+
return raw !== "clean" && TONES.includes(raw) ? raw : void 0;
|
|
29
|
+
}
|
|
16
30
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
17
31
|
var DEFAULT_TEMPO = 120;
|
|
18
32
|
var MIN_TEMPO = 20;
|
|
@@ -252,10 +266,24 @@ function normalizeChords(raw, subdivisions) {
|
|
|
252
266
|
}
|
|
253
267
|
return out;
|
|
254
268
|
}
|
|
269
|
+
var MAX_SONG_MEMO_LENGTH = 1e4;
|
|
270
|
+
var DocumentValidationError = class extends Error {
|
|
271
|
+
};
|
|
272
|
+
function normalizeSongMemo(raw) {
|
|
273
|
+
if (typeof raw !== "string") return void 0;
|
|
274
|
+
const memo = raw.replace(/\r\n?/g, "\n");
|
|
275
|
+
if (memo.length > MAX_SONG_MEMO_LENGTH) {
|
|
276
|
+
throw new DocumentValidationError(`\u66F2\u30E1\u30E2\u306F${MAX_SONG_MEMO_LENGTH}\u6587\u5B57\u4EE5\u5185\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
|
|
277
|
+
}
|
|
278
|
+
return memo.trim() ? memo : void 0;
|
|
279
|
+
}
|
|
255
280
|
function normalizeDocument(data) {
|
|
256
281
|
if (!data || !Array.isArray(data.tuning) || !Array.isArray(data.measures)) {
|
|
257
282
|
throw new Error("Not a valid EZTab document");
|
|
258
283
|
}
|
|
284
|
+
const memo = normalizeSongMemo(data.memo);
|
|
285
|
+
if (memo === void 0) delete data.memo;
|
|
286
|
+
else data.memo = memo;
|
|
259
287
|
data.measures = data.measures.map((m) => {
|
|
260
288
|
let beats = m.beats;
|
|
261
289
|
let rawSubs = m.subdivisions;
|
|
@@ -278,14 +306,14 @@ function normalizeDocument(data) {
|
|
|
278
306
|
);
|
|
279
307
|
const rawChords = Array.isArray(m.chords) ? m.chords : typeof m.chord === "string" && m.chord ? [m.chord] : [];
|
|
280
308
|
const chords = normalizeChords(rawChords, subdivisions);
|
|
281
|
-
const
|
|
309
|
+
const memo2 = normalizeMeasureMemo(m.memo);
|
|
282
310
|
return {
|
|
283
311
|
beats,
|
|
284
312
|
beatUnit,
|
|
285
313
|
subdivisions,
|
|
286
314
|
cells,
|
|
287
315
|
chords,
|
|
288
|
-
...
|
|
316
|
+
...memo2 !== void 0 ? { memo: memo2 } : {}
|
|
289
317
|
};
|
|
290
318
|
});
|
|
291
319
|
if (!Array.isArray(data.keys) || data.keys.length === 0) {
|
|
@@ -306,12 +334,15 @@ function normalizeDocument(data) {
|
|
|
306
334
|
}
|
|
307
335
|
function normalizeTrackMeta(raw, fallbackName) {
|
|
308
336
|
const volume = Number(raw?.volume);
|
|
309
|
-
|
|
337
|
+
const meta = {
|
|
310
338
|
id: typeof raw?.id === "string" && raw.id ? raw.id : newTabId(),
|
|
311
339
|
name: typeof raw?.name === "string" && raw.name.trim() ? raw.name : fallbackName,
|
|
312
340
|
volume: Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : 1,
|
|
313
341
|
muted: raw?.muted === true
|
|
314
342
|
};
|
|
343
|
+
const tone = normalizeTone(raw?.tone);
|
|
344
|
+
if (tone) meta.tone = tone;
|
|
345
|
+
return meta;
|
|
315
346
|
}
|
|
316
347
|
function defaultMainTrack() {
|
|
317
348
|
return { id: newTabId(), name: DEFAULT_MAIN_TRACK_NAME, volume: 1, muted: false };
|
|
@@ -536,7 +567,7 @@ function mainMeasures(doc) {
|
|
|
536
567
|
}
|
|
537
568
|
function closeWorkspace(doc) {
|
|
538
569
|
const ws = doc.workspace;
|
|
539
|
-
if (!ws) return doc;
|
|
570
|
+
if (!ws) return closeWorkspace(openWorkspace(doc));
|
|
540
571
|
const bodies = trackBodies(doc);
|
|
541
572
|
const main = bodies[0];
|
|
542
573
|
const {
|
|
@@ -546,6 +577,7 @@ function closeWorkspace(doc) {
|
|
|
546
577
|
tracks: _tr,
|
|
547
578
|
formatRevision,
|
|
548
579
|
tempoChanges: rawTempoChanges,
|
|
580
|
+
memo: rawMemo,
|
|
549
581
|
...rest
|
|
550
582
|
} = doc;
|
|
551
583
|
const tempoChanges = normalizeTempoChanges(doc.tempo, rawTempoChanges, main.measures.length);
|
|
@@ -554,6 +586,8 @@ function closeWorkspace(doc) {
|
|
|
554
586
|
tuning: [...main.tuning],
|
|
555
587
|
measures: main.measures.map(storedMeasure)
|
|
556
588
|
};
|
|
589
|
+
const memo = normalizeSongMemo(rawMemo);
|
|
590
|
+
if (memo !== void 0) out.memo = memo;
|
|
557
591
|
const extras = bodies.slice(1).map(
|
|
558
592
|
(b, i) => ({
|
|
559
593
|
...ws.metas[i + 1],
|
|
@@ -562,16 +596,18 @@ function closeWorkspace(doc) {
|
|
|
562
596
|
})
|
|
563
597
|
);
|
|
564
598
|
const mainMeta = ws.metas[0];
|
|
565
|
-
const mainIsDefault = mainMeta.name === DEFAULT_MAIN_TRACK_NAME && mainMeta.volume === 1 && !mainMeta.muted;
|
|
599
|
+
const mainIsDefault = mainMeta.name === DEFAULT_MAIN_TRACK_NAME && mainMeta.volume === 1 && !mainMeta.muted && !mainMeta.tone;
|
|
566
600
|
if (extras.length > 0 || !mainIsDefault) out.mainTrack = mainMeta;
|
|
567
601
|
if (extras.length > 0) out.tracks = extras;
|
|
568
602
|
if (tempoChanges) out.tempoChanges = tempoChanges;
|
|
569
603
|
let required = 1;
|
|
604
|
+
if (memo !== void 0) required = SONG_MEMO_FORMAT_REVISION;
|
|
570
605
|
if (extras.length > 0) required = Math.max(required, MULTI_TRACK_FORMAT_REVISION);
|
|
571
606
|
if (bodies.some((b) => measuresHaveHarmonics(b.measures))) {
|
|
572
607
|
required = Math.max(required, HARMONICS_FORMAT_REVISION);
|
|
573
608
|
}
|
|
574
609
|
if (tempoChanges) required = Math.max(required, TEMPO_CHANGES_FORMAT_REVISION);
|
|
610
|
+
if (ws.metas.some((m) => m.tone)) required = Math.max(required, TONE_FORMAT_REVISION);
|
|
575
611
|
if (out.measures.some((m) => m.memo !== void 0)) {
|
|
576
612
|
required = Math.max(required, MEASURE_MEMOS_FORMAT_REVISION);
|
|
577
613
|
}
|
|
@@ -672,6 +708,10 @@ function addTrack(doc, name, tuning) {
|
|
|
672
708
|
}))
|
|
673
709
|
};
|
|
674
710
|
const meta = { id: newTabId(), name, volume: 1, muted: false };
|
|
711
|
+
return appendTrack(doc, meta, body);
|
|
712
|
+
}
|
|
713
|
+
function appendTrack(doc, meta, body) {
|
|
714
|
+
const ws = doc.workspace;
|
|
675
715
|
const added = {
|
|
676
716
|
...doc,
|
|
677
717
|
workspace: { ...ws, metas: [...ws.metas, meta], parked: [...ws.parked, body] }
|
|
@@ -688,6 +728,16 @@ function removeTrack(doc, index) {
|
|
|
688
728
|
const active = w.active > index ? w.active - 1 : w.active;
|
|
689
729
|
return { ...shown, workspace: { active, metas, parked } };
|
|
690
730
|
}
|
|
731
|
+
function setTrackTone(doc, index, tone) {
|
|
732
|
+
const ws = doc.workspace;
|
|
733
|
+
if (!ws || !ws.metas[index]) return doc;
|
|
734
|
+
const metas = ws.metas.map((m, i) => {
|
|
735
|
+
if (i !== index) return m;
|
|
736
|
+
const { tone: _tone, ...rest } = m;
|
|
737
|
+
return tone === "clean" ? rest : { ...rest, tone };
|
|
738
|
+
});
|
|
739
|
+
return { ...doc, workspace: { ...ws, metas } };
|
|
740
|
+
}
|
|
691
741
|
function storedSize(doc) {
|
|
692
742
|
return new TextEncoder().encode(JSON.stringify(closeWorkspace(doc))).length;
|
|
693
743
|
}
|
|
@@ -908,11 +958,13 @@ function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
|
908
958
|
lines.push(`Tracks (track index: name, tuning top\u2192bottom) measures=${main.measures.length}`);
|
|
909
959
|
metas.forEach((t, i) => {
|
|
910
960
|
lines.push(
|
|
911
|
-
` ${i}: ${t.name} ${bodies[i].tuning.join(" ")}${i === 0 ? " (main: carries the chords)" : ""}`
|
|
961
|
+
` ${i}: ${t.name}${t.tone ? ` [${t.tone}]` : ""} ${bodies[i].tuning.join(" ")}${i === 0 ? " (main: carries the chords)" : ""}`
|
|
912
962
|
);
|
|
913
963
|
});
|
|
914
964
|
} else {
|
|
915
|
-
lines.push(
|
|
965
|
+
lines.push(
|
|
966
|
+
`Tuning (top\u2192bottom): ${main.tuning.join(" ")} measures=${main.measures.length}${metas[0].tone ? ` tone=${metas[0].tone}` : ""}`
|
|
967
|
+
);
|
|
916
968
|
}
|
|
917
969
|
const ann = [];
|
|
918
970
|
main.measures.forEach((_, m) => {
|
|
@@ -1021,7 +1073,7 @@ var state = {
|
|
|
1021
1073
|
source: null
|
|
1022
1074
|
};
|
|
1023
1075
|
var server = new McpServer(
|
|
1024
|
-
{ name: "eztab", version: "0.
|
|
1076
|
+
{ name: "eztab", version: "0.6.0" },
|
|
1025
1077
|
{
|
|
1026
1078
|
instructions: "All indices are 0-based: measure 0 is the first measure, string 0 is the top (highest) string, and column 0 is the first column. Notes and chords share the same column grid. Subdivisions are per beat and can differ within a measure (e.g. [2,2,3,2] mixes a triplet beat into straight eighths), so columns per measure = the sum of its per-beat subdivisions; get_tab's ruler shows the column indices. add_measures `at` is a 0-based insert position too. No need to ask the user about indexing. Exception: write_measures uses guitar string numbers (1 = top line), matching the labels get_tab prints. To write whole measures (e.g. transcribing a score or an image), prefer write_measures over set_cells \u2014 it takes one string per beat and does the column arithmetic for you. For transcribing from an image, follow the import_tab_from_image prompt. To deliver a result to the EZTab app, call send_to_eztab; to fix an existing tab, list_tabs \u2192 open_tab \u2192 edit \u2192 send_to_eztab. A tab can hold up to 4 tracks (parts, e.g. guitar + bass); track 0 is the main track. Tools that edit notes take an optional 0-based `track` (default 0). Measures and time signatures are shared by all tracks (adding/removing a measure affects every track); subdivisions are per track; chords belong to track 0 and use its columns."
|
|
1027
1079
|
}
|
|
@@ -1510,6 +1562,21 @@ server.tool(
|
|
|
1510
1562
|
doc.measures = doc.measures.map((m) => resizeMeasureStrings(m, a.tuning.length));
|
|
1511
1563
|
}, a.track)
|
|
1512
1564
|
);
|
|
1565
|
+
var toneParam = z.enum(TONES).describe(
|
|
1566
|
+
"How the track sounds in playback. What a value means follows the track's instrument (a track named or tuned like a bass is a bass): clean; crunch = light overdrive (bass: overdrive); distortion; acoustic = acoustic guitar (bass: upright bass); piano; synth = synth lead (bass: synth bass); strings."
|
|
1567
|
+
);
|
|
1568
|
+
server.tool(
|
|
1569
|
+
"set_tone",
|
|
1570
|
+
"Set a track's playback tone (clean, crunch, distortion, acoustic, piano, synth, strings). Only the sound changes; the notes stay as written.",
|
|
1571
|
+
{ tone: toneParam, track: trackParam },
|
|
1572
|
+
async (a) => {
|
|
1573
|
+
const doc = edit(0, () => {
|
|
1574
|
+
});
|
|
1575
|
+
onTrack(doc, a.track);
|
|
1576
|
+
state.doc = setTrackTone(doc, a.track ?? 0, a.tone);
|
|
1577
|
+
return text(render(state.doc));
|
|
1578
|
+
}
|
|
1579
|
+
);
|
|
1513
1580
|
var TRACK_PRESETS = [
|
|
1514
1581
|
"Guitar (6)",
|
|
1515
1582
|
"Guitar (7)",
|
|
@@ -1524,17 +1591,19 @@ server.tool(
|
|
|
1524
1591
|
{
|
|
1525
1592
|
name: z.string().min(1).describe("Shown in the app, e.g. 'Bass' or 'Guitar 2'."),
|
|
1526
1593
|
preset: z.enum(TRACK_PRESETS).optional(),
|
|
1527
|
-
tuning: z.array(z.string()).min(1).optional().describe("Top (highest) to bottom; overrides preset.")
|
|
1594
|
+
tuning: z.array(z.string()).min(1).optional().describe("Top (highest) to bottom; overrides preset."),
|
|
1595
|
+
tone: toneParam.optional().describe("Playback tone (see set_tone); default clean.")
|
|
1528
1596
|
},
|
|
1529
1597
|
async (a) => {
|
|
1530
1598
|
const before = trackMetas(requireDoc()).length;
|
|
1531
1599
|
const tuning = a.tuning ?? PRESET_TUNINGS[a.preset ?? "Bass (4)"];
|
|
1532
1600
|
const doc = edit(0, () => {
|
|
1533
1601
|
});
|
|
1534
|
-
const
|
|
1535
|
-
if (trackMetas(
|
|
1602
|
+
const added = addTrack(doc, a.name, tuning);
|
|
1603
|
+
if (trackMetas(added).length === before) {
|
|
1536
1604
|
throw new Error("a tab can hold at most 4 tracks");
|
|
1537
1605
|
}
|
|
1606
|
+
const next = setTrackTone(added, before, a.tone ?? "clean");
|
|
1538
1607
|
state.doc = next;
|
|
1539
1608
|
return text(`Added track ${before} "${a.name}".
|
|
1540
1609
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hidechae/eztab-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for EZTab
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "MCP server for EZTab — lets an AI agent build and edit guitar/bass tablature.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"eztab-mcp": "dist/server.js"
|