@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 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 (/bass|ベース/i.test(name)) return "bass";
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.0" },
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: z.array(z.string()).optional(),
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 ?? (a.preset ? PRESET_TUNINGS[a.preset] : void 0);
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: 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)
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: z.array(z.string()).min(1).optional().describe("Top (highest) to bottom; overrides preset."),
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 ?? PRESET_TUNINGS[a.preset ?? "Bass (4)"];
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hidechae/eztab-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "MCP server for EZTab — lets an AI agent build and edit guitar/bass tablature.",
5
5
  "type": "module",
6
6
  "bin": {