@hidechae/eztab-mcp 0.6.1 → 0.7.1
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 +17 -0
- package/dist/server.js +93 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
103
103
|
| `set_tuning` | Replace tuning / string count (`track` で対象パートを指定) |
|
|
104
104
|
| `add_track` / `remove_track` | パート(ベース・2本目のギターなど)の追加・削除。メインを含めて最大4、track 0(メイン)は削除不可 |
|
|
105
105
|
| `set_tone` | パートの再生音色を設定する(`track` で対象パートを指定)。`add_track` でも `tone` を指定できる |
|
|
106
|
+
| `set_backing_style` | 伴奏(コード)の音を設定する: `pad`(既定)/ `guitar` / `piano` / `bass`(ルートのみ) |
|
|
106
107
|
|
|
107
108
|
### 複数パート(トラック)
|
|
108
109
|
|
|
@@ -118,6 +119,14 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
118
119
|
対応上限を超える形式のタブは閲覧のみで、編集・送信できない。
|
|
119
120
|
0.2.0以前のMCPは複数パートのタブを送り返せない(サーバが409で拒否する)。
|
|
120
121
|
|
|
122
|
+
### チューニングの音名
|
|
123
|
+
|
|
124
|
+
チューニングの開放弦は、オクターブの数字を付けた音名で指定します(標準のギターは `E4 B3 G3 D3 A2 E2`、
|
|
125
|
+
4弦ベースは `G2 D2 A1 E1`)。数字の無い音名はすべて同じオクターブとして鳴ってしまうため、
|
|
126
|
+
0.7.1以降は `new_tab` / `set_tuning` / `add_track` が、弦が上から下へ低くなるようにオクターブを補います
|
|
127
|
+
(音名がプリセットと一致すればそのオクターブ)。音名として読めない値はエラーになります。
|
|
128
|
+
アプリも、数字の無いチューニングを持つ既存のタブを開くときに同じ補正をします。
|
|
129
|
+
|
|
121
130
|
### パートの音色
|
|
122
131
|
|
|
123
132
|
0.6.0以降はパートごとの再生音色(形式リビジョン7、docs/design/0010-track-tones.md)を扱えます。
|
|
@@ -131,6 +140,14 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
131
140
|
(1パートのタブでは `tone=distortion`)
|
|
132
141
|
- 0.5.2以前のMCPは、クリーン以外の音色を持つタブを編集・送信できない
|
|
133
142
|
|
|
143
|
+
### 伴奏の音
|
|
144
|
+
|
|
145
|
+
0.7.0以降は伴奏(コード)の音(docs/design/0011-backing-styles.md)を `set_backing_style` で設定できます。
|
|
146
|
+
|
|
147
|
+
- `pad`(既定。従来の音)/ `guitar` / `piano` / `bass`(コードの構成音を鳴らさず、ルートだけをベースで弾く。分数コードは分母の音)
|
|
148
|
+
- `get_tab` はパッド以外のとき、先頭行に `backing=guitar` のように表示する
|
|
149
|
+
- 形式リビジョンは変わらない。0.6.1以前のMCPでも、設定されたタブを編集・送信でき、値は保持される
|
|
150
|
+
|
|
134
151
|
### 小節メモの保持
|
|
135
152
|
|
|
136
153
|
0.5.1以降はアプリで入力した小節共通メモ(形式リビジョン5)を保持して編集・送信できます。
|
package/dist/server.js
CHANGED
|
@@ -28,6 +28,15 @@ function normalizeTone(raw) {
|
|
|
28
28
|
return raw !== "clean" && TONES.includes(raw) ? raw : void 0;
|
|
29
29
|
}
|
|
30
30
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
31
|
+
var BACKING_STYLES = ["pad", "guitar", "piano", "bass"];
|
|
32
|
+
function normalizeBackingStyle(raw) {
|
|
33
|
+
return raw !== "pad" && BACKING_STYLES.includes(raw) ? raw : void 0;
|
|
34
|
+
}
|
|
35
|
+
function withBackingStyle(doc, style) {
|
|
36
|
+
const { backingStyle: _old, ...rest } = doc;
|
|
37
|
+
const stored = normalizeBackingStyle(style);
|
|
38
|
+
return stored ? { ...rest, backingStyle: stored } : rest;
|
|
39
|
+
}
|
|
31
40
|
var DEFAULT_TEMPO = 120;
|
|
32
41
|
var MIN_TEMPO = 20;
|
|
33
42
|
var MAX_TEMPO = 400;
|
|
@@ -281,6 +290,7 @@ function normalizeDocument(data) {
|
|
|
281
290
|
if (!data || !Array.isArray(data.tuning) || !Array.isArray(data.measures)) {
|
|
282
291
|
throw new Error("Not a valid EZTab document");
|
|
283
292
|
}
|
|
293
|
+
data.tuning = completeTuning(data.tuning, namedBass(data.mainTrack?.name));
|
|
284
294
|
const memo = normalizeSongMemo(data.memo);
|
|
285
295
|
if (memo === void 0) delete data.memo;
|
|
286
296
|
else data.memo = memo;
|
|
@@ -328,6 +338,9 @@ function normalizeDocument(data) {
|
|
|
328
338
|
if (!data.mix || typeof data.mix.melody !== "number") {
|
|
329
339
|
data.mix = { ...DEFAULT_MIX };
|
|
330
340
|
}
|
|
341
|
+
const backingStyle = normalizeBackingStyle(data.backingStyle);
|
|
342
|
+
if (backingStyle) data.backingStyle = backingStyle;
|
|
343
|
+
else delete data.backingStyle;
|
|
331
344
|
normalizeTracks(data);
|
|
332
345
|
data.version = TAB_FILE_VERSION;
|
|
333
346
|
return data;
|
|
@@ -360,7 +373,7 @@ function normalizeTracks(data) {
|
|
|
360
373
|
}
|
|
361
374
|
const measures = data.measures;
|
|
362
375
|
data.tracks = rawTracks.slice(0, MAX_TRACKS - 1).map((t, i) => {
|
|
363
|
-
const tuning = Array.isArray(t?.tuning) && t.tuning.length > 0 && t.tuning.every((n) => typeof n === "string") ? [...t.tuning] : [...PRESET_TUNINGS["Guitar (6)"]];
|
|
376
|
+
const tuning = Array.isArray(t?.tuning) && t.tuning.length > 0 && t.tuning.every((n) => typeof n === "string") ? completeTuning([...t.tuning], namedBass(t?.name)) : [...PRESET_TUNINGS["Guitar (6)"]];
|
|
364
377
|
const rawMeasures = Array.isArray(t?.measures) ? t.measures : [];
|
|
365
378
|
if (rawMeasures.length !== measures.length) data.trackMismatch = true;
|
|
366
379
|
return {
|
|
@@ -414,6 +427,51 @@ var PRESET_TUNING_GROUPS = [
|
|
|
414
427
|
var PRESET_TUNINGS = Object.fromEntries(
|
|
415
428
|
PRESET_TUNING_GROUPS.flatMap((g) => g.items.map((i) => [i.name, i.tuning]))
|
|
416
429
|
);
|
|
430
|
+
var OPEN_NOTE = /^([A-Ga-g])([#♯b♭]*)(-?\d+)?$/;
|
|
431
|
+
var LETTER_SEMITONES = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
432
|
+
function parseOpenNote(raw) {
|
|
433
|
+
const m = OPEN_NOTE.exec(typeof raw === "string" ? raw.trim() : "");
|
|
434
|
+
if (!m) return null;
|
|
435
|
+
let semis = LETTER_SEMITONES[m[1].toUpperCase()];
|
|
436
|
+
for (const ch of m[2]) semis += ch === "#" || ch === "\u266F" ? 1 : -1;
|
|
437
|
+
return {
|
|
438
|
+
name: m[1].toUpperCase() + m[2],
|
|
439
|
+
semis,
|
|
440
|
+
pc: (semis % 12 + 12) % 12,
|
|
441
|
+
midi: m[3] != null ? (parseInt(m[3], 10) + 1) * 12 + semis : null
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function completeTuning(tuning, bass = false) {
|
|
445
|
+
const parsed = tuning.map(parseOpenNote);
|
|
446
|
+
if (parsed.some((n) => n === null) || parsed.every((n) => n?.midi != null)) return tuning;
|
|
447
|
+
const notes = parsed;
|
|
448
|
+
const spell = (i, midi) => notes[i].midi !== null ? tuning[i] : notes[i].name + ((midi - notes[i].semis) / 12 - 1);
|
|
449
|
+
const above = (pc, floor) => floor + ((pc - floor) % 12 + 12) % 12;
|
|
450
|
+
const below = (pc, ceil) => ceil - ((ceil - pc) % 12 + 12) % 12;
|
|
451
|
+
const midis = notes.map((n) => n.midi);
|
|
452
|
+
if (notes.every((n) => n.midi === null)) {
|
|
453
|
+
for (const preset of PRESET_TUNING_GROUPS.flatMap((g) => g.items)) {
|
|
454
|
+
const open = preset.tuning.map(parseOpenNote);
|
|
455
|
+
if (open.length === notes.length && open.every((o, i) => o?.pc === notes[i].pc)) {
|
|
456
|
+
return notes.map((_, i) => spell(i, open[i]?.midi));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const reference = bass || notes.length <= 5 ? 43 : 64;
|
|
460
|
+
const up = above(notes[0].pc, reference);
|
|
461
|
+
midis[0] = up - reference <= 6 ? up : up - 12;
|
|
462
|
+
}
|
|
463
|
+
const anchor = midis.findIndex((m) => m !== null);
|
|
464
|
+
for (let i = anchor - 1; i >= 0; i--) {
|
|
465
|
+
midis[i] = above(notes[i].pc, midis[i + 1] + 1);
|
|
466
|
+
}
|
|
467
|
+
for (let i = anchor + 1; i < midis.length; i++) {
|
|
468
|
+
if (midis[i] === null) midis[i] = below(notes[i].pc, midis[i - 1] - 1);
|
|
469
|
+
}
|
|
470
|
+
return notes.map((_, i) => spell(i, midis[i]));
|
|
471
|
+
}
|
|
472
|
+
function namedBass(name) {
|
|
473
|
+
return typeof name === "string" && /bass|ベース/i.test(name);
|
|
474
|
+
}
|
|
417
475
|
var DEFAULT_BEATS = 4;
|
|
418
476
|
var DEFAULT_BEAT_UNIT = 4;
|
|
419
477
|
var DEFAULT_SUBDIVISIONS = 2;
|
|
@@ -769,7 +827,7 @@ function storedSize(doc) {
|
|
|
769
827
|
|
|
770
828
|
// ../src/lib/player.ts
|
|
771
829
|
function instrumentOf(tuning, name = "") {
|
|
772
|
-
if (
|
|
830
|
+
if (namedBass(name)) return "bass";
|
|
773
831
|
const open = tuning.map(noteToMidi).filter((m) => m != null);
|
|
774
832
|
return open.length > 0 && Math.max(...open) < 50 ? "bass" : "guitar";
|
|
775
833
|
}
|
|
@@ -1100,7 +1158,9 @@ function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
|
1100
1158
|
const main = bodies[0];
|
|
1101
1159
|
const shown = bodies.map((_, i) => i).filter((i) => track == null || i === track);
|
|
1102
1160
|
const lines = [];
|
|
1103
|
-
lines.push(
|
|
1161
|
+
lines.push(
|
|
1162
|
+
`Title: ${doc.title}${doc.artist ? ` \u2014 ${doc.artist}` : ""} tempo=${doc.tempo}${doc.backingStyle ? ` backing=${doc.backingStyle}` : ""}`
|
|
1163
|
+
);
|
|
1104
1164
|
if (multi) {
|
|
1105
1165
|
lines.push(`Tracks (track index: name, tuning top\u2192bottom) measures=${main.measures.length}`);
|
|
1106
1166
|
metas.forEach((t, i) => {
|
|
@@ -1220,7 +1280,7 @@ var state = {
|
|
|
1220
1280
|
source: null
|
|
1221
1281
|
};
|
|
1222
1282
|
var server = new McpServer(
|
|
1223
|
-
{ name: "eztab", version: "0.
|
|
1283
|
+
{ name: "eztab", version: "0.7.1" },
|
|
1224
1284
|
{
|
|
1225
1285
|
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."
|
|
1226
1286
|
}
|
|
@@ -1271,6 +1331,9 @@ function setDoc(doc) {
|
|
|
1271
1331
|
return text(render(state.doc));
|
|
1272
1332
|
}
|
|
1273
1333
|
var MISMATCH_NOTE = "Warning: this tab's extra tracks may be shifted against the main track \u2014 an older EZTab app added or removed measures without them. Check that the tracks line up (get_tab) and ask the user before sending it back.";
|
|
1334
|
+
var tuningParam = z.array(z.string().regex(/^[A-Ga-g][#♯b♭]*(-?\d+)?$/, "a note name such as E2, F#3 or Bb1")).min(1).describe(
|
|
1335
|
+
'Open notes from the top (highest) string to the bottom (lowest), each WITH its octave, e.g. standard guitar ["E4","B3","G3","D3","A2","E2"], bass ["G2","D2","A1","E1"]. A missing octave is filled in so the strings run from high to low.'
|
|
1336
|
+
);
|
|
1274
1337
|
var trackParam = z.number().int().min(0).optional().describe("0-based track (part) index from get_tab; default 0, the main track.");
|
|
1275
1338
|
function assertMeasure(doc, m) {
|
|
1276
1339
|
if (!Number.isInteger(m) || m < 0 || m >= doc.measures.length) {
|
|
@@ -1300,12 +1363,12 @@ server.tool(
|
|
|
1300
1363
|
title: z.string().optional(),
|
|
1301
1364
|
artist: z.string().optional(),
|
|
1302
1365
|
preset: z.enum(["Guitar (6)", "Bass (4)", "Bass (5)", "Ukulele (4)"]).optional(),
|
|
1303
|
-
tuning:
|
|
1366
|
+
tuning: tuningParam.optional(),
|
|
1304
1367
|
measures: z.number().int().min(1).max(512).optional(),
|
|
1305
1368
|
tempo: z.number().int().min(20).max(400).optional()
|
|
1306
1369
|
},
|
|
1307
1370
|
async (a) => {
|
|
1308
|
-
const tuning = a.tuning
|
|
1371
|
+
const tuning = a.tuning ? completeTuning(a.tuning) : a.preset ? PRESET_TUNINGS[a.preset] : void 0;
|
|
1309
1372
|
const doc = createDocument(tuning, a.measures ?? 4);
|
|
1310
1373
|
if (a.title) doc.title = a.title;
|
|
1311
1374
|
if (a.artist) doc.artist = a.artist;
|
|
@@ -1703,11 +1766,15 @@ server.tool(
|
|
|
1703
1766
|
server.tool(
|
|
1704
1767
|
"set_tuning",
|
|
1705
1768
|
"Replace the tuning / string count. Existing frets are preserved where strings overlap. Order is top (highest) to bottom (lowest).",
|
|
1706
|
-
{ tuning:
|
|
1707
|
-
async (a) =>
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1769
|
+
{ tuning: tuningParam, track: trackParam },
|
|
1770
|
+
async (a) => {
|
|
1771
|
+
const name = trackMetas(requireDoc())[a.track ?? 0]?.name;
|
|
1772
|
+
const tuning = completeTuning(a.tuning, namedBass(name));
|
|
1773
|
+
return applyEdit((doc) => {
|
|
1774
|
+
doc.tuning = tuning;
|
|
1775
|
+
doc.measures = doc.measures.map((m) => resizeMeasureStrings(m, tuning.length));
|
|
1776
|
+
}, a.track);
|
|
1777
|
+
}
|
|
1711
1778
|
);
|
|
1712
1779
|
var toneParam = z.enum(TONES).describe(
|
|
1713
1780
|
"How the track sounds in playback. Guitar tracks: clean, crunch (light overdrive), distortion, acoustic, piano, synth, strings. Bass tracks (named or tuned like a bass): clean, crunch (overdrive), acoustic (upright bass), synth (synth bass) only."
|
|
@@ -1734,6 +1801,17 @@ server.tool(
|
|
|
1734
1801
|
return text(render(state.doc));
|
|
1735
1802
|
}
|
|
1736
1803
|
);
|
|
1804
|
+
server.tool(
|
|
1805
|
+
"set_backing_style",
|
|
1806
|
+
"Set how the chord backing (the chords written with set_chords / @Name) sounds in playback: pad (the default soft pad), guitar, piano, or bass (only the root \u2014 or a slash chord's bass note \u2014 on a bass).",
|
|
1807
|
+
{ style: z.enum(BACKING_STYLES) },
|
|
1808
|
+
async (a) => {
|
|
1809
|
+
const doc = edit(0, () => {
|
|
1810
|
+
});
|
|
1811
|
+
state.doc = withBackingStyle(doc, a.style);
|
|
1812
|
+
return text(render(state.doc));
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1737
1815
|
var TRACK_PRESETS = [
|
|
1738
1816
|
"Guitar (6)",
|
|
1739
1817
|
"Guitar (7)",
|
|
@@ -1748,12 +1826,14 @@ server.tool(
|
|
|
1748
1826
|
{
|
|
1749
1827
|
name: z.string().min(1).describe("Shown in the app, e.g. 'Bass' or 'Guitar 2'."),
|
|
1750
1828
|
preset: z.enum(TRACK_PRESETS).optional(),
|
|
1751
|
-
tuning:
|
|
1829
|
+
tuning: tuningParam.optional().describe(
|
|
1830
|
+
"Overrides preset. Open notes top (highest) to bottom, each WITH its octave (e.g. G2 D2 A1 E1)."
|
|
1831
|
+
),
|
|
1752
1832
|
tone: toneParam.optional().describe("Playback tone (see set_tone); default clean.")
|
|
1753
1833
|
},
|
|
1754
1834
|
async (a) => {
|
|
1755
1835
|
const before = trackMetas(requireDoc()).length;
|
|
1756
|
-
const tuning = a.tuning
|
|
1836
|
+
const tuning = a.tuning ? completeTuning(a.tuning, namedBass(a.name)) : PRESET_TUNINGS[a.preset ?? "Bass (4)"];
|
|
1757
1837
|
const doc = edit(0, () => {
|
|
1758
1838
|
});
|
|
1759
1839
|
const added = addTrack(doc, a.name, tuning);
|