@hidechae/eztab-mcp 0.7.0 → 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 +8 -0
- package/dist/server.js +67 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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)を扱えます。
|
package/dist/server.js
CHANGED
|
@@ -290,6 +290,7 @@ function normalizeDocument(data) {
|
|
|
290
290
|
if (!data || !Array.isArray(data.tuning) || !Array.isArray(data.measures)) {
|
|
291
291
|
throw new Error("Not a valid EZTab document");
|
|
292
292
|
}
|
|
293
|
+
data.tuning = completeTuning(data.tuning, namedBass(data.mainTrack?.name));
|
|
293
294
|
const memo = normalizeSongMemo(data.memo);
|
|
294
295
|
if (memo === void 0) delete data.memo;
|
|
295
296
|
else data.memo = memo;
|
|
@@ -372,7 +373,7 @@ function normalizeTracks(data) {
|
|
|
372
373
|
}
|
|
373
374
|
const measures = data.measures;
|
|
374
375
|
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)"]];
|
|
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)"]];
|
|
376
377
|
const rawMeasures = Array.isArray(t?.measures) ? t.measures : [];
|
|
377
378
|
if (rawMeasures.length !== measures.length) data.trackMismatch = true;
|
|
378
379
|
return {
|
|
@@ -426,6 +427,51 @@ var PRESET_TUNING_GROUPS = [
|
|
|
426
427
|
var PRESET_TUNINGS = Object.fromEntries(
|
|
427
428
|
PRESET_TUNING_GROUPS.flatMap((g) => g.items.map((i) => [i.name, i.tuning]))
|
|
428
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
|
+
}
|
|
429
475
|
var DEFAULT_BEATS = 4;
|
|
430
476
|
var DEFAULT_BEAT_UNIT = 4;
|
|
431
477
|
var DEFAULT_SUBDIVISIONS = 2;
|
|
@@ -781,7 +827,7 @@ function storedSize(doc) {
|
|
|
781
827
|
|
|
782
828
|
// ../src/lib/player.ts
|
|
783
829
|
function instrumentOf(tuning, name = "") {
|
|
784
|
-
if (
|
|
830
|
+
if (namedBass(name)) return "bass";
|
|
785
831
|
const open = tuning.map(noteToMidi).filter((m) => m != null);
|
|
786
832
|
return open.length > 0 && Math.max(...open) < 50 ? "bass" : "guitar";
|
|
787
833
|
}
|
|
@@ -1234,7 +1280,7 @@ var state = {
|
|
|
1234
1280
|
source: null
|
|
1235
1281
|
};
|
|
1236
1282
|
var server = new McpServer(
|
|
1237
|
-
{ name: "eztab", version: "0.7.
|
|
1283
|
+
{ name: "eztab", version: "0.7.1" },
|
|
1238
1284
|
{
|
|
1239
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."
|
|
1240
1286
|
}
|
|
@@ -1285,6 +1331,9 @@ function setDoc(doc) {
|
|
|
1285
1331
|
return text(render(state.doc));
|
|
1286
1332
|
}
|
|
1287
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
|
+
);
|
|
1288
1337
|
var trackParam = z.number().int().min(0).optional().describe("0-based track (part) index from get_tab; default 0, the main track.");
|
|
1289
1338
|
function assertMeasure(doc, m) {
|
|
1290
1339
|
if (!Number.isInteger(m) || m < 0 || m >= doc.measures.length) {
|
|
@@ -1314,12 +1363,12 @@ server.tool(
|
|
|
1314
1363
|
title: z.string().optional(),
|
|
1315
1364
|
artist: z.string().optional(),
|
|
1316
1365
|
preset: z.enum(["Guitar (6)", "Bass (4)", "Bass (5)", "Ukulele (4)"]).optional(),
|
|
1317
|
-
tuning:
|
|
1366
|
+
tuning: tuningParam.optional(),
|
|
1318
1367
|
measures: z.number().int().min(1).max(512).optional(),
|
|
1319
1368
|
tempo: z.number().int().min(20).max(400).optional()
|
|
1320
1369
|
},
|
|
1321
1370
|
async (a) => {
|
|
1322
|
-
const tuning = a.tuning
|
|
1371
|
+
const tuning = a.tuning ? completeTuning(a.tuning) : a.preset ? PRESET_TUNINGS[a.preset] : void 0;
|
|
1323
1372
|
const doc = createDocument(tuning, a.measures ?? 4);
|
|
1324
1373
|
if (a.title) doc.title = a.title;
|
|
1325
1374
|
if (a.artist) doc.artist = a.artist;
|
|
@@ -1717,11 +1766,15 @@ server.tool(
|
|
|
1717
1766
|
server.tool(
|
|
1718
1767
|
"set_tuning",
|
|
1719
1768
|
"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
|
-
|
|
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
|
+
}
|
|
1725
1778
|
);
|
|
1726
1779
|
var toneParam = z.enum(TONES).describe(
|
|
1727
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."
|
|
@@ -1773,12 +1826,14 @@ server.tool(
|
|
|
1773
1826
|
{
|
|
1774
1827
|
name: z.string().min(1).describe("Shown in the app, e.g. 'Bass' or 'Guitar 2'."),
|
|
1775
1828
|
preset: z.enum(TRACK_PRESETS).optional(),
|
|
1776
|
-
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
|
+
),
|
|
1777
1832
|
tone: toneParam.optional().describe("Playback tone (see set_tone); default clean.")
|
|
1778
1833
|
},
|
|
1779
1834
|
async (a) => {
|
|
1780
1835
|
const before = trackMetas(requireDoc()).length;
|
|
1781
|
-
const tuning = a.tuning
|
|
1836
|
+
const tuning = a.tuning ? completeTuning(a.tuning, namedBass(a.name)) : PRESET_TUNINGS[a.preset ?? "Bass (4)"];
|
|
1782
1837
|
const doc = edit(0, () => {
|
|
1783
1838
|
});
|
|
1784
1839
|
const added = addTrack(doc, a.name, tuning);
|