@hidechae/eztab-mcp 0.7.0 → 0.8.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 +11 -1
- package/dist/server.js +108 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +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
|
+
| `set_backing_style` | 伴奏(コード)の音を設定する: `pad`(既定)/ `guitar` / `acoustic` / `piano` / `bass`(ルートのみ)/ `upright`(ルートのみ)/ `synth` / `strings` |
|
|
107
107
|
|
|
108
108
|
### 複数パート(トラック)
|
|
109
109
|
|
|
@@ -119,6 +119,14 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
119
119
|
対応上限を超える形式のタブは閲覧のみで、編集・送信できない。
|
|
120
120
|
0.2.0以前のMCPは複数パートのタブを送り返せない(サーバが409で拒否する)。
|
|
121
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
|
+
|
|
122
130
|
### パートの音色
|
|
123
131
|
|
|
124
132
|
0.6.0以降はパートごとの再生音色(形式リビジョン7、docs/design/0010-track-tones.md)を扱えます。
|
|
@@ -137,6 +145,8 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
137
145
|
0.7.0以降は伴奏(コード)の音(docs/design/0011-backing-styles.md)を `set_backing_style` で設定できます。
|
|
138
146
|
|
|
139
147
|
- `pad`(既定。従来の音)/ `guitar` / `piano` / `bass`(コードの構成音を鳴らさず、ルートだけをベースで弾く。分数コードは分母の音)
|
|
148
|
+
- 0.8.0以降は `acoustic`(アコースティックギター)/ `upright`(ウッドベース。`bass` と同じくルートだけ)/ `synth` / `strings` も選べます。
|
|
149
|
+
0.7.x 以前の版でこれらの値のタブを開くと、値は取り除かれてパッドに戻ります
|
|
140
150
|
- `get_tab` はパッド以外のとき、先頭行に `backing=guitar` のように表示する
|
|
141
151
|
- 形式リビジョンは変わらない。0.6.1以前のMCPでも、設定されたタブを編集・送信でき、値は保持される
|
|
142
152
|
|
package/dist/server.js
CHANGED
|
@@ -28,7 +28,16 @@ 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 = [
|
|
31
|
+
var BACKING_STYLES = [
|
|
32
|
+
"pad",
|
|
33
|
+
"guitar",
|
|
34
|
+
"acoustic",
|
|
35
|
+
"piano",
|
|
36
|
+
"bass",
|
|
37
|
+
"upright",
|
|
38
|
+
"synth",
|
|
39
|
+
"strings"
|
|
40
|
+
];
|
|
32
41
|
function normalizeBackingStyle(raw) {
|
|
33
42
|
return raw !== "pad" && BACKING_STYLES.includes(raw) ? raw : void 0;
|
|
34
43
|
}
|
|
@@ -290,6 +299,7 @@ function normalizeDocument(data) {
|
|
|
290
299
|
if (!data || !Array.isArray(data.tuning) || !Array.isArray(data.measures)) {
|
|
291
300
|
throw new Error("Not a valid EZTab document");
|
|
292
301
|
}
|
|
302
|
+
data.tuning = completeTuning(data.tuning, namedBass(data.mainTrack?.name));
|
|
293
303
|
const memo = normalizeSongMemo(data.memo);
|
|
294
304
|
if (memo === void 0) delete data.memo;
|
|
295
305
|
else data.memo = memo;
|
|
@@ -344,10 +354,10 @@ function normalizeDocument(data) {
|
|
|
344
354
|
data.version = TAB_FILE_VERSION;
|
|
345
355
|
return data;
|
|
346
356
|
}
|
|
347
|
-
function normalizeTrackMeta(raw, fallbackName) {
|
|
357
|
+
function normalizeTrackMeta(raw, fallbackName, missingIdPrefix = "") {
|
|
348
358
|
const volume = Number(raw?.volume);
|
|
349
359
|
const meta = {
|
|
350
|
-
id: typeof raw?.id === "string" && raw.id ? raw.id : newTabId(),
|
|
360
|
+
id: typeof raw?.id === "string" && raw.id ? raw.id : missingIdPrefix + newTabId(),
|
|
351
361
|
name: typeof raw?.name === "string" && raw.name.trim() ? raw.name : fallbackName,
|
|
352
362
|
volume: Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : 1,
|
|
353
363
|
muted: raw?.muted === true
|
|
@@ -372,11 +382,11 @@ function normalizeTracks(data) {
|
|
|
372
382
|
}
|
|
373
383
|
const measures = data.measures;
|
|
374
384
|
data.tracks = rawTracks.slice(0, MAX_TRACKS - 1).map((t, i) => {
|
|
375
|
-
const tuning = Array.isArray(t?.tuning) && t.tuning.length > 0 && t.tuning.every((n) => typeof n === "string") ? [...t.tuning] : [...PRESET_TUNINGS["Guitar (6)"]];
|
|
385
|
+
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)"]];
|
|
376
386
|
const rawMeasures = Array.isArray(t?.measures) ? t.measures : [];
|
|
377
387
|
if (rawMeasures.length !== measures.length) data.trackMismatch = true;
|
|
378
388
|
return {
|
|
379
|
-
...normalizeTrackMeta(t, `Track ${i + 2}
|
|
389
|
+
...normalizeTrackMeta(t, `Track ${i + 2}`, "legacy:"),
|
|
380
390
|
tuning,
|
|
381
391
|
measures: measures.map((m, mi) => fitTrackMeasure(rawMeasures[mi], m, tuning.length))
|
|
382
392
|
};
|
|
@@ -426,6 +436,51 @@ var PRESET_TUNING_GROUPS = [
|
|
|
426
436
|
var PRESET_TUNINGS = Object.fromEntries(
|
|
427
437
|
PRESET_TUNING_GROUPS.flatMap((g) => g.items.map((i) => [i.name, i.tuning]))
|
|
428
438
|
);
|
|
439
|
+
var OPEN_NOTE = /^([A-Ga-g])([#♯b♭]*)(-?\d+)?$/;
|
|
440
|
+
var LETTER_SEMITONES = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
441
|
+
function parseOpenNote(raw) {
|
|
442
|
+
const m = OPEN_NOTE.exec(typeof raw === "string" ? raw.trim() : "");
|
|
443
|
+
if (!m) return null;
|
|
444
|
+
let semis = LETTER_SEMITONES[m[1].toUpperCase()];
|
|
445
|
+
for (const ch of m[2]) semis += ch === "#" || ch === "\u266F" ? 1 : -1;
|
|
446
|
+
return {
|
|
447
|
+
name: m[1].toUpperCase() + m[2],
|
|
448
|
+
semis,
|
|
449
|
+
pc: (semis % 12 + 12) % 12,
|
|
450
|
+
midi: m[3] != null ? (parseInt(m[3], 10) + 1) * 12 + semis : null
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function completeTuning(tuning, bass = false) {
|
|
454
|
+
const parsed = tuning.map(parseOpenNote);
|
|
455
|
+
if (parsed.some((n) => n === null) || parsed.every((n) => n?.midi != null)) return tuning;
|
|
456
|
+
const notes = parsed;
|
|
457
|
+
const spell = (i, midi) => notes[i].midi !== null ? tuning[i] : notes[i].name + ((midi - notes[i].semis) / 12 - 1);
|
|
458
|
+
const above = (pc, floor) => floor + ((pc - floor) % 12 + 12) % 12;
|
|
459
|
+
const below = (pc, ceil) => ceil - ((ceil - pc) % 12 + 12) % 12;
|
|
460
|
+
const midis = notes.map((n) => n.midi);
|
|
461
|
+
if (notes.every((n) => n.midi === null)) {
|
|
462
|
+
for (const preset of PRESET_TUNING_GROUPS.flatMap((g) => g.items)) {
|
|
463
|
+
const open = preset.tuning.map(parseOpenNote);
|
|
464
|
+
if (open.length === notes.length && open.every((o, i) => o?.pc === notes[i].pc)) {
|
|
465
|
+
return notes.map((_, i) => spell(i, open[i]?.midi));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const reference = bass || notes.length <= 5 ? 43 : 64;
|
|
469
|
+
const up = above(notes[0].pc, reference);
|
|
470
|
+
midis[0] = up - reference <= 6 ? up : up - 12;
|
|
471
|
+
}
|
|
472
|
+
const anchor = midis.findIndex((m) => m !== null);
|
|
473
|
+
for (let i = anchor - 1; i >= 0; i--) {
|
|
474
|
+
midis[i] = above(notes[i].pc, midis[i + 1] + 1);
|
|
475
|
+
}
|
|
476
|
+
for (let i = anchor + 1; i < midis.length; i++) {
|
|
477
|
+
if (midis[i] === null) midis[i] = below(notes[i].pc, midis[i - 1] - 1);
|
|
478
|
+
}
|
|
479
|
+
return notes.map((_, i) => spell(i, midis[i]));
|
|
480
|
+
}
|
|
481
|
+
function namedBass(name) {
|
|
482
|
+
return typeof name === "string" && /bass|ベース/i.test(name);
|
|
483
|
+
}
|
|
429
484
|
var DEFAULT_BEATS = 4;
|
|
430
485
|
var DEFAULT_BEAT_UNIT = 4;
|
|
431
486
|
var DEFAULT_SUBDIVISIONS = 2;
|
|
@@ -540,6 +595,9 @@ function noteToMidi(note) {
|
|
|
540
595
|
// ../src/lib/playbackRate.ts
|
|
541
596
|
var PLAYBACK_RATES = Array.from({ length: 36 }, (_, i) => (5 + i) / 20);
|
|
542
597
|
|
|
598
|
+
// ../src/lib/strum.ts
|
|
599
|
+
var MAX_SHARE = 1 / 6;
|
|
600
|
+
|
|
543
601
|
// ../src/lib/tracks.ts
|
|
544
602
|
function newUid() {
|
|
545
603
|
return newTabId();
|
|
@@ -781,7 +839,7 @@ function storedSize(doc) {
|
|
|
781
839
|
|
|
782
840
|
// ../src/lib/player.ts
|
|
783
841
|
function instrumentOf(tuning, name = "") {
|
|
784
|
-
if (
|
|
842
|
+
if (namedBass(name)) return "bass";
|
|
785
843
|
const open = tuning.map(noteToMidi).filter((m) => m != null);
|
|
786
844
|
return open.length > 0 && Math.max(...open) < 50 ? "bass" : "guitar";
|
|
787
845
|
}
|
|
@@ -804,6 +862,7 @@ var GUITAR_CLEAN = {
|
|
|
804
862
|
cabHz: OPEN,
|
|
805
863
|
cabQ: 0,
|
|
806
864
|
lowCutHz: 70,
|
|
865
|
+
lowCutQ: 0,
|
|
807
866
|
level: 1
|
|
808
867
|
};
|
|
809
868
|
var BASS_CLEAN = { ...GUITAR_CLEAN, pickupHz: 2600, pickupQ: 1, lowCutHz: 30 };
|
|
@@ -826,26 +885,33 @@ var CHAINS = {
|
|
|
826
885
|
lowCutHz: 80,
|
|
827
886
|
level: 0.26
|
|
828
887
|
},
|
|
888
|
+
// A deep, soft high gain (a Friedman or a Hughes & Kettner rather than a
|
|
889
|
+
// bright Marshall): the lows mostly stay in, both stages are driven hard,
|
|
890
|
+
// and the top is rolled off early instead of pushed at the presence range.
|
|
829
891
|
distortion: {
|
|
830
892
|
...GUITAR_CLEAN,
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
893
|
+
pickupHz: 3800,
|
|
894
|
+
pickupQ: 1,
|
|
895
|
+
tightHz: 140,
|
|
896
|
+
pushDb: 5,
|
|
834
897
|
drive: 0.5,
|
|
835
|
-
stage1: { gain:
|
|
898
|
+
stage1: { gain: 30, bias: 0.2 },
|
|
836
899
|
// The next tube stage is inverted, so it clips the other half harder.
|
|
837
900
|
drive2: 1,
|
|
838
|
-
stage2: { gain:
|
|
839
|
-
interHpHz:
|
|
840
|
-
interLpHz:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
901
|
+
stage2: { gain: 8, bias: -0.3 },
|
|
902
|
+
interHpHz: 110,
|
|
903
|
+
interLpHz: 4200,
|
|
904
|
+
// Low mids for body, where the bright voicing had a presence peak.
|
|
905
|
+
bodyHz: 400,
|
|
906
|
+
bodyDb: 2,
|
|
907
|
+
cabHz: 3800,
|
|
908
|
+
cabQ: 1,
|
|
909
|
+
lowCutHz: 85,
|
|
910
|
+
lowCutQ: 2,
|
|
911
|
+
level: 0.15
|
|
847
912
|
},
|
|
848
|
-
|
|
913
|
+
// The body is in the waveform itself (stringSynth's acousticParams).
|
|
914
|
+
acoustic: { ...GUITAR_CLEAN, pickupHz: 12e3, pickupQ: 0, level: 1.2 },
|
|
849
915
|
piano: PIANO,
|
|
850
916
|
synth: { ...DIRECT, level: 0.8 },
|
|
851
917
|
strings: { ...DIRECT, level: 0.8 }
|
|
@@ -879,15 +945,8 @@ var CHAINS = {
|
|
|
879
945
|
lowCutHz: 35,
|
|
880
946
|
level: 0.15
|
|
881
947
|
},
|
|
882
|
-
// Upright bass.
|
|
883
|
-
acoustic: {
|
|
884
|
-
...BASS_CLEAN,
|
|
885
|
-
pickupHz: 3e3,
|
|
886
|
-
pickupQ: 0,
|
|
887
|
-
bodyHz: 100,
|
|
888
|
-
bodyDb: 6,
|
|
889
|
-
level: 1.4
|
|
890
|
-
},
|
|
948
|
+
// Upright bass. The body is in the waveform itself (stringSynth's uprightParams).
|
|
949
|
+
acoustic: { ...BASS_CLEAN, pickupHz: 5e3, pickupQ: 0, level: 1.4 },
|
|
891
950
|
piano: PIANO,
|
|
892
951
|
// Synth bass.
|
|
893
952
|
synth: { ...DIRECT, level: 0.8 },
|
|
@@ -1234,7 +1293,7 @@ var state = {
|
|
|
1234
1293
|
source: null
|
|
1235
1294
|
};
|
|
1236
1295
|
var server = new McpServer(
|
|
1237
|
-
{ name: "eztab", version: "0.
|
|
1296
|
+
{ name: "eztab", version: "0.8.0" },
|
|
1238
1297
|
{
|
|
1239
1298
|
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."
|
|
1240
1299
|
}
|
|
@@ -1285,6 +1344,9 @@ function setDoc(doc) {
|
|
|
1285
1344
|
return text(render(state.doc));
|
|
1286
1345
|
}
|
|
1287
1346
|
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.";
|
|
1347
|
+
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(
|
|
1348
|
+
'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.'
|
|
1349
|
+
);
|
|
1288
1350
|
var trackParam = z.number().int().min(0).optional().describe("0-based track (part) index from get_tab; default 0, the main track.");
|
|
1289
1351
|
function assertMeasure(doc, m) {
|
|
1290
1352
|
if (!Number.isInteger(m) || m < 0 || m >= doc.measures.length) {
|
|
@@ -1314,12 +1376,12 @@ server.tool(
|
|
|
1314
1376
|
title: z.string().optional(),
|
|
1315
1377
|
artist: z.string().optional(),
|
|
1316
1378
|
preset: z.enum(["Guitar (6)", "Bass (4)", "Bass (5)", "Ukulele (4)"]).optional(),
|
|
1317
|
-
tuning:
|
|
1379
|
+
tuning: tuningParam.optional(),
|
|
1318
1380
|
measures: z.number().int().min(1).max(512).optional(),
|
|
1319
1381
|
tempo: z.number().int().min(20).max(400).optional()
|
|
1320
1382
|
},
|
|
1321
1383
|
async (a) => {
|
|
1322
|
-
const tuning = a.tuning
|
|
1384
|
+
const tuning = a.tuning ? completeTuning(a.tuning) : a.preset ? PRESET_TUNINGS[a.preset] : void 0;
|
|
1323
1385
|
const doc = createDocument(tuning, a.measures ?? 4);
|
|
1324
1386
|
if (a.title) doc.title = a.title;
|
|
1325
1387
|
if (a.artist) doc.artist = a.artist;
|
|
@@ -1717,11 +1779,15 @@ server.tool(
|
|
|
1717
1779
|
server.tool(
|
|
1718
1780
|
"set_tuning",
|
|
1719
1781
|
"Replace the tuning / string count. Existing frets are preserved where strings overlap. Order is top (highest) to bottom (lowest).",
|
|
1720
|
-
{ tuning:
|
|
1721
|
-
async (a) =>
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1782
|
+
{ tuning: tuningParam, track: trackParam },
|
|
1783
|
+
async (a) => {
|
|
1784
|
+
const name = trackMetas(requireDoc())[a.track ?? 0]?.name;
|
|
1785
|
+
const tuning = completeTuning(a.tuning, namedBass(name));
|
|
1786
|
+
return applyEdit((doc) => {
|
|
1787
|
+
doc.tuning = tuning;
|
|
1788
|
+
doc.measures = doc.measures.map((m) => resizeMeasureStrings(m, tuning.length));
|
|
1789
|
+
}, a.track);
|
|
1790
|
+
}
|
|
1725
1791
|
);
|
|
1726
1792
|
var toneParam = z.enum(TONES).describe(
|
|
1727
1793
|
"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."
|
|
@@ -1750,7 +1816,7 @@ server.tool(
|
|
|
1750
1816
|
);
|
|
1751
1817
|
server.tool(
|
|
1752
1818
|
"set_backing_style",
|
|
1753
|
-
"Set how the chord backing (the chords written with set_chords / @Name) sounds in playback: pad (the default soft pad), guitar, piano, or
|
|
1819
|
+
"Set how the chord backing (the chords written with set_chords / @Name) sounds in playback: pad (the default soft pad), guitar (clean electric), acoustic (acoustic guitar), piano, synth, strings, or only the root \u2014 or a slash chord's bass note \u2014 on a bass (electric) or upright (upright bass).",
|
|
1754
1820
|
{ style: z.enum(BACKING_STYLES) },
|
|
1755
1821
|
async (a) => {
|
|
1756
1822
|
const doc = edit(0, () => {
|
|
@@ -1773,12 +1839,14 @@ server.tool(
|
|
|
1773
1839
|
{
|
|
1774
1840
|
name: z.string().min(1).describe("Shown in the app, e.g. 'Bass' or 'Guitar 2'."),
|
|
1775
1841
|
preset: z.enum(TRACK_PRESETS).optional(),
|
|
1776
|
-
tuning:
|
|
1842
|
+
tuning: tuningParam.optional().describe(
|
|
1843
|
+
"Overrides preset. Open notes top (highest) to bottom, each WITH its octave (e.g. G2 D2 A1 E1)."
|
|
1844
|
+
),
|
|
1777
1845
|
tone: toneParam.optional().describe("Playback tone (see set_tone); default clean.")
|
|
1778
1846
|
},
|
|
1779
1847
|
async (a) => {
|
|
1780
1848
|
const before = trackMetas(requireDoc()).length;
|
|
1781
|
-
const tuning = a.tuning
|
|
1849
|
+
const tuning = a.tuning ? completeTuning(a.tuning, namedBass(a.name)) : PRESET_TUNINGS[a.preset ?? "Bass (4)"];
|
|
1782
1850
|
const doc = edit(0, () => {
|
|
1783
1851
|
});
|
|
1784
1852
|
const added = addTrack(doc, a.name, tuning);
|