@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.
Files changed (3) hide show
  1. package/README.md +11 -1
  2. package/dist/server.js +108 -40
  3. 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 = ["pad", "guitar", "piano", "bass"];
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 (/bass|ベース/i.test(name)) return "bass";
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
- pickupQ: 2,
832
- tightHz: 250,
833
- pushDb: 6,
893
+ pickupHz: 3800,
894
+ pickupQ: 1,
895
+ tightHz: 140,
896
+ pushDb: 5,
834
897
  drive: 0.5,
835
- stage1: { gain: 24, bias: 0.25 },
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: 5, bias: -0.35 },
839
- interHpHz: 160,
840
- interLpHz: 6500,
841
- bodyHz: 2800,
842
- bodyDb: 4,
843
- cabHz: 4800,
844
- cabQ: 3,
845
- lowCutHz: 90,
846
- level: 0.17
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
- acoustic: { ...GUITAR_CLEAN, pickupHz: 9e3, pickupQ: 0, bodyDb: 5, level: 1.2 },
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.7.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: z.array(z.string()).optional(),
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 ?? (a.preset ? PRESET_TUNINGS[a.preset] : void 0);
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: z.array(z.string()).min(1), track: trackParam },
1721
- async (a) => applyEdit((doc) => {
1722
- doc.tuning = [...a.tuning];
1723
- doc.measures = doc.measures.map((m) => resizeMeasureStrings(m, a.tuning.length));
1724
- }, a.track)
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 bass (only the root \u2014 or a slash chord's bass note \u2014 on a bass).",
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: z.array(z.string()).min(1).optional().describe("Top (highest) to bottom; overrides preset."),
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 ?? PRESET_TUNINGS[a.preset ?? "Bass (4)"];
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hidechae/eztab-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server for EZTab — lets an AI agent build and edit guitar/bass tablature.",
5
5
  "type": "module",
6
6
  "bin": {