friday-mcp-v2 3.0.3 → 3.0.5

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.
@@ -48,6 +48,14 @@ export function getBridgeLogPath() {
48
48
  return join(os.homedir(), '.local', 'share', 'friday-mcp', 'logs', 'bridge.log');
49
49
  }
50
50
 
51
+ export function getToolLogPath() {
52
+ if (process.platform === 'win32') {
53
+ const localAppData = process.env.LOCALAPPDATA || join(os.homedir(), 'AppData', 'Local');
54
+ return join(localAppData, 'FridayMCP', 'logs', 'tool-calls.jsonl');
55
+ }
56
+ return join(os.homedir(), '.local', 'share', 'friday-mcp', 'logs', 'tool-calls.jsonl');
57
+ }
58
+
51
59
  export function getBridgePidPath() {
52
60
  if (process.platform === 'win32') {
53
61
  const localAppData = process.env.LOCALAPPDATA || join(os.homedir(), 'AppData', 'Local');
@@ -12,9 +12,10 @@ import { FridayWPClient, ConnectionRegistry } from "./wordpress-api.js";
12
12
  import { BridgeClient } from "./bridge-client.js";
13
13
  import fetch from "node-fetch";
14
14
  import { randomUUID } from "node:crypto";
15
- import { readFileSync, statSync, realpathSync } from "node:fs";
15
+ import { readFileSync, statSync, realpathSync, appendFileSync, mkdirSync, existsSync, writeFileSync, renameSync } from "node:fs";
16
16
  import { execFile } from "node:child_process";
17
- import { resolve as resolvePath, sep, dirname } from "node:path";
17
+ import { resolve as resolvePath, sep, dirname, join as joinPath } from "node:path";
18
+ import os from "node:os";
18
19
  import { fileURLToPath } from "node:url";
19
20
  // package.json からバージョンを取得
20
21
  const __pkg_dirname = dirname(fileURLToPath(import.meta.url));
@@ -48,6 +49,39 @@ if (_envWorkspace) {
48
49
  }
49
50
  }
50
51
 
52
+ // ========================================
53
+ // Tool Call Logger(FRIDAY_DEBUG=1 で有効)
54
+ // ========================================
55
+ const _TOOL_LOG_MAX_BYTES = 10 * 1024 * 1024; // 10MB
56
+ function _getToolLogPath() {
57
+ if (process.platform === 'win32') {
58
+ const localAppData = process.env.LOCALAPPDATA || joinPath(os.homedir(), 'AppData', 'Local');
59
+ return joinPath(localAppData, 'FridayMCP', 'logs', 'tool-calls.jsonl');
60
+ }
61
+ return joinPath(os.homedir(), '.local', 'share', 'friday-mcp', 'logs', 'tool-calls.jsonl');
62
+ }
63
+
64
+ function writeToolLog(record) {
65
+ if (process.env.FRIDAY_DEBUG !== '1') return;
66
+ try {
67
+ const logPath = _getToolLogPath();
68
+ const logDir = dirname(logPath);
69
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
70
+ // ローテーション: 10MB 超えたら先頭半分を切り捨て
71
+ try {
72
+ const st = statSync(logPath);
73
+ if (st.size > _TOOL_LOG_MAX_BYTES) {
74
+ const content = readFileSync(logPath, 'utf-8');
75
+ const lines = content.split('\n');
76
+ const half = Math.floor(lines.length / 2);
77
+ writeFileSync(logPath, lines.slice(half).join('\n'), 'utf-8');
78
+ }
79
+ } catch (_) { /* ファイル未存在時は無視 */ }
80
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n';
81
+ appendFileSync(logPath, line, 'utf-8');
82
+ } catch (_) { /* ログ書き込み失敗は無視 */ }
83
+ }
84
+
51
85
  const registry = new ConnectionRegistry();
52
86
  const bridgeClient = new BridgeClient();
53
87
 
@@ -215,7 +249,7 @@ function checkRevisionMismatch(expectedRevision, currentState, mode, postId, ses
215
249
  : '';
216
250
  const text = `❌ REVISION_MISMATCH: expected ${expectedRevision}, actual ${currentRevision}` +
217
251
  snapshotLine +
218
- `\n→ get_article_structure で最新を取得してください`;
252
+ `\n→ Re-fetch via get_article_structure.`;
219
253
  return { content: [{ type: "text", text }], isError: true };
220
254
  }
221
255
 
@@ -695,24 +729,6 @@ const targetSchema = {
695
729
  type: "boolean",
696
730
  description: "Target user-selected block. Editor mode only.",
697
731
  },
698
- index: {
699
- type: "number",
700
- description: "Block index (0-based, flattened).",
701
- },
702
- indices: {
703
- type: "array",
704
- items: { type: "number" },
705
- description: "Multiple indices (0-based).",
706
- },
707
- range: {
708
- type: "object",
709
- properties: {
710
- start: { type: "number", description: "Start (inclusive)." },
711
- end: { type: "number", description: "End (inclusive)." },
712
- },
713
- required: ["start", "end"],
714
- description: "Index range.",
715
- },
716
732
  section: {
717
733
  type: "string",
718
734
  description: "Section by heading text (partial match).",
@@ -748,7 +764,7 @@ const targetSchema = {
748
764
  description: "Multiple block refs from snapshot. Requires snapshotId.",
749
765
  },
750
766
  },
751
- description: "Target: one of selected/index/indices/range/heading/ref/refs + optional filters.",
767
+ description: "Target: one of selected/ref/refs/heading/section/blockType + optional filters (contains/nth). Requires snapshotId when using ref/refs.",
752
768
  };
753
769
 
754
770
  const targetSchemaNoSelected = {
@@ -756,7 +772,7 @@ const targetSchemaNoSelected = {
756
772
  properties: { ...targetSchema.properties },
757
773
  };
758
774
  delete targetSchemaNoSelected.properties.selected;
759
- targetSchemaNoSelected.description = "Target: one of index/range/heading + optional filters.";
775
+ targetSchemaNoSelected.description = "Target: one of ref/refs/heading/section/blockType + optional filters (contains/nth). Requires snapshotId when using ref/refs.";
760
776
 
761
777
  const insertSchema = {
762
778
  type: "object",
@@ -781,9 +797,6 @@ function normalizeTarget(target) {
781
797
  // --- 排他バリデーション ---
782
798
  const primaries = [
783
799
  target.selected && 'selected',
784
- target.index !== undefined && 'index',
785
- target.indices && 'indices',
786
- target.range && 'range',
787
800
  target.heading && 'heading',
788
801
  target.ref && 'ref',
789
802
  target.refs && 'refs',
@@ -796,19 +809,6 @@ function normalizeTarget(target) {
796
809
  if (target.ref) result._ref = target.ref;
797
810
  if (target.refs) result._refs = target.refs;
798
811
  if (target.selected) result.target = "selected";
799
- if (target.index !== undefined) result.index = target.index;
800
- if (target.indices) result.indices = target.indices;
801
- if (target.range) {
802
- const s = target.range.start;
803
- const e = target.range.end;
804
- if (s === undefined || s === null || e === undefined || e === null ||
805
- typeof s !== "number" || typeof e !== "number" ||
806
- !Number.isInteger(s) || !Number.isInteger(e) || s < 0 || e < 0) {
807
- throw new Error("range.start と range.end は 0 以上の整数で両方指定してください。");
808
- }
809
- result.startIndex = s;
810
- result.endIndex = e;
811
- }
812
812
  if (target.section) result.section = target.section;
813
813
  if (target.blockType) result.blockType = target.blockType;
814
814
  if (target.nth !== undefined) result.typeIndex = target.nth;
@@ -897,15 +897,15 @@ function resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentSt
897
897
  // 1. snapshot 取得
898
898
  const snap = snapshotCache.get(snapshotId);
899
899
  if (!snap) {
900
- throw new Error(`snapshot "${snapshotId}" が見つかりません(期限切れまたは無効)。get_article_structure を再取得してください。`);
900
+ throw new Error(`snapshot "${snapshotId}" が見つかりません(期限切れまたは無効)。Re-fetch via get_article_structure.`);
901
901
  }
902
902
 
903
903
  // 2. 状態ソース束縛チェック
904
904
  if (snap.mode !== mode) {
905
- throw new Error(`snapshot は ${snap.mode} モードで取得されましたが、現在は ${mode} モードです。get_article_structure を再取得してください。`);
905
+ throw new Error(`snapshot は ${snap.mode} モードで取得されましたが、現在は ${mode} モードです。Re-fetch via get_article_structure.`);
906
906
  }
907
907
  if (mode === 'editor' && snap.sessionId && sessionId && snap.sessionId !== sessionId) {
908
- throw new Error(`snapshot は sessionId "${snap.sessionId}" で取得されましたが、現在の sessionId は "${sessionId}" です。get_article_structure を再取得してください。`);
908
+ throw new Error(`snapshot は sessionId "${snap.sessionId}" で取得されましたが、現在の sessionId は "${sessionId}" です。Re-fetch via get_article_structure.`);
909
909
  }
910
910
  if (snap.postId !== postId) {
911
911
  throw new Error(`snapshot は postId ${snap.postId} 用ですが、postId ${postId} に対して使用されています。`);
@@ -944,14 +944,14 @@ function resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentSt
944
944
  if (refined.length === 1) return refined[0].index;
945
945
  throw new Error(
946
946
  `ref "${ref}" (type: ${snapBlock.type}) の解決先が ${refined.length > 0 ? refined.length : candidates.length} 件見つかりました。` +
947
- `一意に特定できません。get_article_structure を再取得してください。`
947
+ `一意に特定できません。Re-fetch via get_article_structure.`
948
948
  );
949
949
  }
950
950
 
951
951
  // 5c. 一致 0件 → エラー
952
952
  throw new Error(
953
953
  `ref "${ref}" (${snapBlock.type}, fingerprint: ${snapBlock.fingerprint}) に一致するブロックが見つかりません。` +
954
- `構造が大幅に変更された可能性があります。get_article_structure を再取得してください。`
954
+ `構造が大幅に変更された可能性があります。Re-fetch via get_article_structure.`
955
955
  );
956
956
  }
957
957
 
@@ -991,12 +991,15 @@ async function resolveRefsAndCheckRevision({
991
991
  mode, client, postId, sessionId, siteName,
992
992
  }) {
993
993
  const hasRef = ref || refs;
994
+ const _debug = process.env.FRIDAY_DEBUG === '1';
995
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: ref=${ref || 'none'}, refs=${refs ? refs.join(',') : 'none'}, snapshotId=${snapshotId || 'none'}, expectedRevision=${expectedRevision || 'none'}`);
994
996
 
995
997
  // Case 1: ref/refs 指定あり
996
998
  if (hasRef) {
997
999
  if (!snapshotId) {
1000
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: ref指定あり but snapshotId missing → error`);
998
1001
  return { error: {
999
- content: [{ type: "text", text: "❌ ref/refs を使用するには snapshotId が必要です。get_article_structure で取得してください。" }],
1002
+ content: [{ type: "text", text: "❌ snapshotId required for ref/refs. Get it from get_article_structure or any prior write response." }],
1000
1003
  isError: true,
1001
1004
  }};
1002
1005
  }
@@ -1007,13 +1010,16 @@ async function resolveRefsAndCheckRevision({
1007
1010
  try {
1008
1011
  if (ref) {
1009
1012
  const index = resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState);
1013
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: ref=${ref} → index=${index}`);
1010
1014
  return { index, currentState };
1011
1015
  }
1012
1016
  if (refs) {
1013
1017
  const indices = refs.map(r => resolveRefFromState(snapshotId, r, mode, sessionId, postId, currentState));
1018
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: refs=[${refs.join(',')}] → indices=[${indices.join(',')}]`);
1014
1019
  return { indices, currentState };
1015
1020
  }
1016
1021
  } catch (e) {
1022
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: ref解決失敗 — ${e.message}`);
1017
1023
  return { error: {
1018
1024
  content: [{ type: "text", text: `❌ ref 解決エラー: ${e.message}` }],
1019
1025
  isError: true,
@@ -1023,6 +1029,7 @@ async function resolveRefsAndCheckRevision({
1023
1029
 
1024
1030
  // Case 2: ref なし + expectedRevision のみ
1025
1031
  if (expectedRevision) {
1032
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: index指定 (revisionチェックのみ)`);
1026
1033
  const { currentState, error } = await acquireFreshState({
1027
1034
  expectedRevision, mode, client, postId, sessionId, siteName,
1028
1035
  });
@@ -1031,6 +1038,7 @@ async function resolveRefsAndCheckRevision({
1031
1038
  }
1032
1039
 
1033
1040
  // Case 3: どちらもなし
1041
+ if (_debug) console.error(`[SNAPSHOT] resolveRefs: ref/revision なし → index直接指定`);
1034
1042
  return {};
1035
1043
  }
1036
1044
 
@@ -1065,8 +1073,10 @@ function normalizeDownstreamBlocks(blocks) {
1065
1073
  * @returns {object|null} { snapshotId, revision, blocks[] } or null
1066
1074
  */
1067
1075
  function buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName) {
1076
+ const _debug = process.env.FRIDAY_DEBUG === '1';
1068
1077
  const blocks = result?.blocks;
1069
1078
  if (!blocks || !Array.isArray(blocks) || blocks.length === 0) {
1079
+ if (_debug) console.error(`[SNAPSHOT] fromResult: blocks=${blocks ? 'empty' : 'missing'}`);
1070
1080
  return null;
1071
1081
  }
1072
1082
  const state = normalizeDownstreamBlocks(blocks);
@@ -1086,19 +1096,26 @@ function buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteNa
1086
1096
  * @returns {Promise<object|null>} { snapshotId, revision, blocks[] } or null
1087
1097
  */
1088
1098
  async function buildResponseSnapshot(mode, client, postId, sessionId, siteName, result) {
1099
+ const _debug = process.env.FRIDAY_DEBUG === '1';
1089
1100
  // Phase 1: 下流 result.blocks があればそこから構築(再取得不要)
1090
1101
  if (result) {
1091
1102
  const fromResult = buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName);
1092
- if (fromResult) return fromResult;
1103
+ if (fromResult) {
1104
+ if (_debug) console.error(`[SNAPSHOT] buildResponseSnapshot: fromResult OK, snapshotId=${fromResult.snapshotId}, blocks=${fromResult.blocks?.length}`);
1105
+ return fromResult;
1106
+ }
1093
1107
  }
1108
+ if (_debug) console.error(`[SNAPSHOT] buildResponseSnapshot: fromResult failed, fallback to getCurrentStructure`);
1094
1109
  let newState;
1095
1110
  try {
1096
1111
  newState = await getCurrentStructure(mode, client, postId, sessionId);
1097
1112
  } catch {
1113
+ if (_debug) console.error(`[SNAPSHOT] buildResponseSnapshot: getCurrentStructure threw → null`);
1098
1114
  return null;
1099
1115
  }
1100
1116
 
1101
1117
  if (!newState?.allBlocks || newState.allBlocks.length === 0) {
1118
+ if (_debug) console.error(`[SNAPSHOT] buildResponseSnapshot: allBlocks empty → null`);
1102
1119
  return null;
1103
1120
  }
1104
1121
 
@@ -1145,7 +1162,12 @@ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName,
1145
1162
  * @returns {string} snapshot 行と blocks 一覧が付加されたテキスト
1146
1163
  */
1147
1164
  function appendSnapshotToTextLegacy(text, snapshot, refInfo) {
1148
- if (!snapshot) return text;
1165
+ const _debug = process.env.FRIDAY_DEBUG === '1';
1166
+ if (!snapshot) {
1167
+ if (_debug) console.error(`[SNAPSHOT] appendLegacy: snapshot=null → タグなし`);
1168
+ return text;
1169
+ }
1170
+ if (_debug) console.error(`[SNAPSHOT] appendLegacy: snapshotId=${snapshot.snapshotId}, blocks=${snapshot.blocks?.length}`);
1149
1171
 
1150
1172
  // 1→N 展開チェック(単体操作時のみ)
1151
1173
  let expandedLine = '';
@@ -1182,9 +1204,12 @@ function appendSnapshotToTextLegacy(text, snapshot, refInfo) {
1182
1204
  * @returns {string}
1183
1205
  */
1184
1206
  function buildUpdateDiffResponse(text, snap, result, preState, inputRef, isInsert, refInfo) {
1207
+ const _debug = process.env.FRIDAY_DEBUG === '1';
1185
1208
  if (!inputRef || !snap) {
1209
+ if (_debug) console.error(`[SNAPSHOT] diffResponse: legacy path (inputRef=${inputRef || 'null'}, snap=${snap ? 'ok' : 'null'})`);
1186
1210
  return appendSnapshotToTextLegacy(text, snap, refInfo);
1187
1211
  }
1212
+ if (_debug) console.error(`[SNAPSHOT] diffResponse: diff path (inputRef=${inputRef}, snapId=${snap?.snapshotId})`);
1188
1213
 
1189
1214
  if (isInsert) {
1190
1215
  const changeInfo = buildChangeInfoFromResult('inserted', snap, result, preState);
@@ -1287,6 +1312,7 @@ function buildUpdateDiffResponse(text, snap, result, preState, inputRef, isInser
1287
1312
  */
1288
1313
  function appendRefChangesToText(text, changeInfo) {
1289
1314
  if (!changeInfo) return text;
1315
+ if (process.env.FRIDAY_DEBUG === '1') console.error(`[SNAPSHOT] appendDiff: type=${changeInfo.type}, snapshotId=${changeInfo.snapshotId}`);
1290
1316
 
1291
1317
  let out = text + `\n\n[snapshot:${changeInfo.snapshotId} rev:${changeInfo.revision}]`;
1292
1318
 
@@ -1305,13 +1331,13 @@ function appendRefChangesToText(text, changeInfo) {
1305
1331
  .join(', ');
1306
1332
  break;
1307
1333
  case 'updated':
1308
- out += `\nupdated: [${changeInfo.updatedRefs.join(', ')}] (use new snapshot)`;
1334
+ out += `\nupdated: [${changeInfo.updatedRefs.join(', ')}] (reuse this snapshotId for next operation)`;
1309
1335
  break;
1310
1336
  case 'expanded':
1311
1337
  out += `\nexpanded: ${changeInfo.expanded.oldRef} \u2192 [${changeInfo.expanded.newRefs.join(', ')}]`;
1312
1338
  break;
1313
1339
  case 'duplicated':
1314
- out += `\nduplicated: ${changeInfo.sourceRef} \u2192 ${changeInfo.newRef} (source:[${changeInfo.sourceIndex}], new:[${changeInfo.newIndex}]) (use new snapshot)`;
1340
+ out += `\nduplicated: ${changeInfo.sourceRef} \u2192 ${changeInfo.newRef} (source:[${changeInfo.sourceIndex}], new:[${changeInfo.newIndex}]) (reuse this snapshotId for next operation)`;
1315
1341
  break;
1316
1342
  }
1317
1343
 
@@ -2029,40 +2055,33 @@ const tools = [
2029
2055
  },
2030
2056
  {
2031
2057
  name: "delete_block",
2032
- description: "Delete block(s) by index, ref, or selection.",
2058
+ description: "Delete block(s) by ref or selection.",
2033
2059
  inputSchema: {
2034
2060
  type: "object",
2035
2061
  properties: {
2036
2062
  postId: postIdParam,
2037
2063
  site: siteParam,
2038
- index: { type: "number", description: "Index (0-based). Omit for selected." },
2039
- count: { type: "number", description: "Consecutive count (default: 1)" },
2040
- indices: { type: "array", items: { type: "number" }, description: "Multiple indices (exclusive with index/count)" },
2041
- snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref/refs." },
2042
- ref: { type: "string", description: "Block ref from snapshot (exclusive with index/indices)." },
2043
- refs: { type: "array", items: { type: "string" }, description: "Multiple block refs (exclusive with index/indices)." },
2064
+ snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required." },
2065
+ ref: { type: "string", description: "Block ref from snapshot." },
2066
+ refs: { type: "array", items: { type: "string" }, description: "Multiple block refs from snapshot." },
2044
2067
  expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
2045
2068
  },
2046
2069
  },
2047
2070
  },
2048
2071
  {
2049
2072
  name: "move_block",
2050
- description: "Move block(s). Use from/to (top-level), fromFlat/toFlat (nested), or fromRef+beforeRef/afterRef (ref-based).",
2073
+ description: "Move block(s). Use fromRef + beforeRef or afterRef. Requires snapshotId.",
2051
2074
  inputSchema: {
2052
2075
  type: "object",
2053
2076
  properties: {
2054
2077
  postId: postIdParam,
2055
2078
  site: siteParam,
2056
- from: { type: "number", description: "Source top-level index" },
2057
- to: { type: "number", description: "Target position" },
2058
- fromFlat: { type: "number", description: "Source flattened index" },
2059
- toFlat: { type: "number", description: "Target flattened position" },
2060
2079
  count: { type: "integer", description: "Consecutive count (default: 1)" },
2061
- snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
2080
+ snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required." },
2062
2081
  expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
2063
- fromRef: { type: "string", description: "Source block ref (exclusive with from/fromFlat)." },
2064
- beforeRef: { type: "string", description: "Move before this ref (exclusive with to/toFlat/afterRef)." },
2065
- afterRef: { type: "string", description: "Move after this ref (exclusive with to/toFlat/beforeRef)." },
2082
+ fromRef: { type: "string", description: "Source block ref." },
2083
+ beforeRef: { type: "string", description: "Move before this ref (exclusive with afterRef)." },
2084
+ afterRef: { type: "string", description: "Move after this ref (exclusive with beforeRef)." },
2066
2085
  },
2067
2086
  },
2068
2087
  },
@@ -2092,15 +2111,14 @@ const tools = [
2092
2111
  },
2093
2112
  {
2094
2113
  name: "duplicate_block",
2095
- description: "Duplicate block by index, ref, or selection.",
2114
+ description: "Duplicate block by ref or selection.",
2096
2115
  inputSchema: {
2097
2116
  type: "object",
2098
2117
  properties: {
2099
2118
  postId: postIdParam,
2100
2119
  site: siteParam,
2101
- index: { type: "number", description: "Index (0-based). Omit for selected." },
2102
- snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
2103
- ref: { type: "string", description: "Block ref from snapshot (exclusive with index)." },
2120
+ snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required." },
2121
+ ref: { type: "string", description: "Block ref from snapshot." },
2104
2122
  expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
2105
2123
  },
2106
2124
  },
@@ -2184,7 +2202,7 @@ const tools = [
2184
2202
  },
2185
2203
  {
2186
2204
  name: "insert_block",
2187
- description: "Insert Gutenberg HTML. Omit index to append. Use beforeRef/afterRef for ref-based positioning.",
2205
+ description: "Insert Gutenberg HTML. Use beforeRef/afterRef for positioning. Omit both to append to end.",
2188
2206
  inputSchema: {
2189
2207
  type: "object",
2190
2208
  properties: {
@@ -2192,12 +2210,10 @@ const tools = [
2192
2210
  site: siteParam,
2193
2211
  rawHTML: { type: "string", description: "HTML to insert (exclusive with filePath)" },
2194
2212
  filePath: { type: "string", description: "Local file path (exclusive with rawHTML)" },
2195
- index: { type: "number", description: "Position (0-based). Omit to append." },
2196
- position: { type: "string", enum: ["before", "after"], description: "Insert relative to index: 'before' (default) or 'after'. Requires index." },
2197
- snapshotId: { type: "string", description: "Snapshot ID. Required when using beforeRef/afterRef." },
2213
+ snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required when using beforeRef/afterRef. Omit to append to end." },
2198
2214
  expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
2199
- beforeRef: { type: "string", description: "Insert before this ref (exclusive with index)." },
2200
- afterRef: { type: "string", description: "Insert after this ref (exclusive with index)." },
2215
+ beforeRef: { type: "string", description: "Insert before this ref." },
2216
+ afterRef: { type: "string", description: "Insert after this ref." },
2201
2217
  },
2202
2218
  },
2203
2219
  },
@@ -2235,7 +2251,7 @@ const tools = [
2235
2251
  site: siteParam,
2236
2252
  snapshotId: {
2237
2253
  type: "string",
2238
- description: "Snapshot ID from get_article_structure. Required when using ref/refs in target.",
2254
+ description: "Snapshot ID (from get_article_structure or any write response). Required when using ref/refs in target.",
2239
2255
  },
2240
2256
  expectedRevision: {
2241
2257
  type: "string",
@@ -2320,16 +2336,12 @@ const tools = [
2320
2336
  },
2321
2337
  {
2322
2338
  name: "table_operations",
2323
- description: "Table operations (get/update/add/delete rows/columns/cells). Use ref+snapshotId for safe index resolution.",
2339
+ description: "Table operations (get/update/add/delete rows/columns/cells). Requires ref+snapshotId to identify the table block.",
2324
2340
  inputSchema: {
2325
2341
  type: "object",
2326
2342
  properties: {
2327
2343
  postId: postIdParam,
2328
2344
  site: siteParam,
2329
- index: {
2330
- type: "number",
2331
- description: "Table index (0-based)",
2332
- },
2333
2345
  action: {
2334
2346
  type: "string",
2335
2347
  enum: ["get_structure", "update_cell", "add_row", "delete_row", "add_column", "delete_column", "move_row", "move_column", "update_row", "update_column"],
@@ -2356,8 +2368,8 @@ const tools = [
2356
2368
  items: { type: "string" },
2357
2369
  description: "add_row: new cells, add_column: init values, update_row/column: replacements. Omit for empty.",
2358
2370
  },
2359
- snapshotId: { type: "string", description: "Snapshot ID from get_article_structure. Required when using ref." },
2360
- ref: { type: "string", description: "Block ref from snapshot (exclusive with index)." },
2371
+ snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required." },
2372
+ ref: { type: "string", description: "Table block ref from snapshot." },
2361
2373
  expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
2362
2374
  },
2363
2375
  required: ["action"],
@@ -2473,7 +2485,7 @@ async function handleUpdateBlocksTool(args, toolName) {
2473
2485
  }
2474
2486
  if (!snapshotId) {
2475
2487
  return {
2476
- content: [{ type: "text", text: `❌ operations には snapshotId が必要です。get_article_structure で取得してください。` }],
2488
+ content: [{ type: "text", text: `❌ snapshotId required for operations. Get it from get_article_structure or any prior write response.` }],
2477
2489
  isError: true,
2478
2490
  };
2479
2491
  }
@@ -2816,10 +2828,12 @@ async function handleUpdateBlocksTool(args, toolName) {
2816
2828
  }
2817
2829
 
2818
2830
  // ツール実行のハンドラ
2831
+ const _WRITE_TOOLS = new Set(['update_blocks', 'delete_block', 'move_block', 'duplicate_block', 'insert_block', 'table_operations']);
2819
2832
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
2820
2833
  const { name, arguments: args } = request.params;
2834
+ const _toolLogStart = Date.now();
2821
2835
  try {
2822
- switch (name) {
2836
+ const _result = await (async () => { switch (name) {
2823
2837
  case "get_article_structure": {
2824
2838
  let { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
2825
2839
  if (mode === 'error') {
@@ -3356,9 +3370,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3356
3370
  if (_guard) return _guard;
3357
3371
  }
3358
3372
 
3359
- // ref/refs index/indices 排他チェック
3360
- if ((args?.ref || args?.refs) && (args?.index !== undefined || args?.indices !== undefined)) {
3361
- return { content: [{ type: "text", text: "❌ ref/refs index/indices は同時に指定できません。" }], isError: true };
3373
+ // ref または refs が必須(選択ブロック削除はeditorコマンド経由で処理)
3374
+ if (!args?.ref && !args?.refs) {
3375
+ return { content: [{ type: "text", text: "❌ ref or refs required, along with snapshotId." }], isError: true };
3362
3376
  }
3363
3377
 
3364
3378
  // ref 解決 + revision チェック
@@ -3370,20 +3384,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3370
3384
  if (resolved.error) return resolved.error;
3371
3385
  const _preState = resolved.currentState || null; // Phase 2: 差分計算用
3372
3386
 
3373
- let { index, count, indices } = args;
3374
- if (resolved.index !== undefined) { index = resolved.index; count = count || 1; }
3375
- if (resolved.indices !== undefined) { indices = resolved.indices; }
3387
+ let index = resolved.index;
3388
+ let indices = resolved.indices;
3389
+ const count = (index !== undefined) ? 1 : undefined;
3376
3390
 
3377
- // Phase 3: 入力 ref を保持(ref + count > 1 は差分化できないので Legacy)
3391
+ // Phase 3: 入力 ref を保持
3378
3392
  const _inputRefs = args?.refs ? [...new Set(args.refs)]
3379
- : (args?.ref && (count || 1) <= 1) ? [args.ref]
3393
+ : args?.ref ? [args.ref]
3380
3394
  : null;
3381
3395
 
3382
- // indices と index/count の排他バリデーション
3383
- if (indices !== undefined && (index !== undefined || count !== undefined)) {
3384
- return { content: [{ type: "text", text: "❌ indices と index/count は同時に指定できません。" }], isError: true };
3385
- }
3386
-
3387
3396
  // indices モード(非連続一括削除)
3388
3397
  if (indices !== undefined) {
3389
3398
  if (!Array.isArray(indices) || indices.length === 0) {
@@ -3428,10 +3437,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3428
3437
  return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3429
3438
  }
3430
3439
 
3431
- // 既存モード(index + count)
3440
+ // 単一 ref モード(ref → index 解決済み)
3432
3441
  if (mode === 'headless') {
3433
3442
  if (index === undefined) {
3434
- return { content: [{ type: "text", text: "❌ Headless モードでは index の指定が必須です。" }], isError: true };
3443
+ return { content: [{ type: "text", text: "❌ ref resolution failed. Check snapshotId and ref." }], isError: true };
3435
3444
  }
3436
3445
  try {
3437
3446
  const result = await client.headlessDelete(postId, index, count || 1);
@@ -3482,8 +3491,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3482
3491
  if (_guard) return _guard;
3483
3492
  }
3484
3493
 
3485
- const { from, to } = args;
3486
- let { fromFlat, toFlat } = args;
3494
+ let fromFlat, toFlat;
3487
3495
  const count = args.count ?? 1;
3488
3496
 
3489
3497
  // count バリデーション
@@ -3491,70 +3499,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3491
3499
  return { content: [{ type: "text", text: "❌ count は1以上の整数を指定してください。" }], isError: true };
3492
3500
  }
3493
3501
 
3494
- // 3 モード排他バリデーション
3495
- const hasTopLevel = from !== undefined || to !== undefined;
3496
- const hasFlat = fromFlat !== undefined || toFlat !== undefined;
3497
- const hasRef = args?.fromRef !== undefined || args?.beforeRef !== undefined || args?.afterRef !== undefined;
3498
- const _modeCount = [hasTopLevel, hasFlat, hasRef].filter(Boolean).length;
3499
-
3500
- if (_modeCount > 1) {
3501
- return { content: [{ type: "text", text: "❌ from/to, fromFlat/toFlat, ref(fromRef/beforeRef/afterRef) は同時に指定できません。" }], isError: true };
3502
+ // fromRef + beforeRef/afterRef 必須チェック
3503
+ if (!args.fromRef) {
3504
+ return { content: [{ type: "text", text: "❌ fromRef is required." }], isError: true };
3502
3505
  }
3503
- if (_modeCount === 0) {
3504
- return { content: [{ type: "text", text: "❌ from/to, fromFlat/toFlat, または fromRef+beforeRef/afterRef を指定してください。" }], isError: true };
3506
+ if (!args.beforeRef && !args.afterRef) {
3507
+ return { content: [{ type: "text", text: "❌ beforeRef or afterRef is required." }], isError: true };
3505
3508
  }
3506
-
3507
- // expectedRevision チェック(全モード共通)
3508
- // ref モード以外でも expectedRevision が指定されていれば revision チェックを実施
3509
- let _preState = null; // Phase 2: 差分計算用
3510
- if (args?.expectedRevision && !hasRef) {
3511
- const { currentState, error: _revError } = await acquireFreshState({
3512
- expectedRevision: args.expectedRevision,
3513
- mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3514
- });
3515
- if (_revError) return _revError;
3516
- _preState = currentState || null;
3509
+ if (args.beforeRef && args.afterRef) {
3510
+ return { content: [{ type: "text", text: "❌ beforeRef と afterRef は同時に指定できません。" }], isError: true };
3511
+ }
3512
+ if (!args.snapshotId) {
3513
+ return { content: [{ type: "text", text: "❌ snapshotId required. Get it from get_article_structure or any prior write response." }], isError: true };
3517
3514
  }
3518
3515
 
3519
- // ref モード: fromRef + beforeRef/afterRef
3520
- if (hasRef) {
3521
- if (!args.fromRef) {
3522
- return { content: [{ type: "text", text: "❌ ref モードでは fromRef が必須です。" }], isError: true };
3523
- }
3524
- if (!args.beforeRef && !args.afterRef) {
3525
- return { content: [{ type: "text", text: "❌ ref モードでは beforeRef または afterRef が必須です。" }], isError: true };
3526
- }
3527
- if (args.beforeRef && args.afterRef) {
3528
- return { content: [{ type: "text", text: "❌ beforeRef と afterRef は同時に指定できません。" }], isError: true };
3529
- }
3530
- if (!args.snapshotId) {
3531
- return { content: [{ type: "text", text: "❌ ref を使用するには snapshotId が必要です。get_article_structure で取得してください。" }], isError: true };
3532
- }
3533
-
3534
- // acquireFreshState で 1 回だけ state 取得 + revision チェック
3535
- const { currentState, error: _stateError } = await acquireFreshState({
3536
- expectedRevision: args.expectedRevision,
3537
- mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3538
- });
3539
- if (_stateError) return _stateError;
3540
- _preState = currentState || null; // Phase 2: 差分計算用
3541
-
3542
- // ref 解決(同じ currentState で 2 つの ref を解決)
3543
- try {
3544
- fromFlat = resolveRefFromState(args.snapshotId, args.fromRef, mode, _sessionId, postId, currentState);
3545
- const destRef = args.beforeRef || args.afterRef;
3546
- const resolvedDest = resolveRefFromState(args.snapshotId, destRef, mode, _sessionId, postId, currentState);
3547
- // 位置計算: beforeRef → そのまま, afterRef → +1
3548
- toFlat = args.beforeRef ? resolvedDest : resolvedDest + 1;
3549
- } catch (e) {
3550
- return { content: [{ type: "text", text: `❌ ref 解決エラー: ${e.message}` }], isError: true };
3551
- }
3516
+ // acquireFreshState state 取得 + revision チェック
3517
+ let _preState = null;
3518
+ const { currentState, error: _stateError } = await acquireFreshState({
3519
+ expectedRevision: args.expectedRevision,
3520
+ mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3521
+ });
3522
+ if (_stateError) return _stateError;
3523
+ _preState = currentState || null;
3552
3524
 
3553
- // ref flat に変換完了。以降 flat モードに合流
3525
+ // ref 解決(同じ currentState 2 つの ref を解決)
3526
+ try {
3527
+ fromFlat = resolveRefFromState(args.snapshotId, args.fromRef, mode, _sessionId, postId, currentState);
3528
+ const destRef = args.beforeRef || args.afterRef;
3529
+ const resolvedDest = resolveRefFromState(args.snapshotId, destRef, mode, _sessionId, postId, currentState);
3530
+ toFlat = args.beforeRef ? resolvedDest : resolvedDest + 1;
3531
+ } catch (e) {
3532
+ return { content: [{ type: "text", text: `❌ ref 解決エラー: ${e.message}` }], isError: true };
3554
3533
  }
3555
3534
 
3556
- // Phase 4: 入力 ref 保持(ref モード + count <= 1 のみ差分化)
3557
- const _inputRefs = (hasRef && count <= 1) ? [args.fromRef] : null;
3535
+ // 入力 ref 保持(count <= 1 のみ差分化)
3536
+ const _inputRefs = (count <= 1) ? [args.fromRef] : null;
3558
3537
 
3559
3538
  // 移動結果のレスポンス生成ヘルパー
3560
3539
  const moveMsg = (moved) => {
@@ -3566,56 +3545,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3566
3545
  : `✅ ブロック移動 (${typeLabel})${_mt_mb}`;
3567
3546
  };
3568
3547
 
3569
- // fromFlat/toFlat モード(フラットインデックス移動)— ref モードもここに合流
3570
- if (hasFlat || hasRef) {
3571
- if (fromFlat === undefined || toFlat === undefined) {
3572
- return { content: [{ type: "text", text: "❌ fromFlat と toFlat の両方を指定してください。" }], isError: true };
3573
- }
3574
- if (fromFlat === toFlat || toFlat === fromFlat + count) {
3575
- return { content: [{ type: "text", text: `✅ 移動不要(同じ位置)${_mt_mb}` }] };
3576
- }
3577
-
3578
- if (mode === 'headless') {
3579
- try {
3580
- const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
3581
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3582
- const _msg = moveMsg(result.moved);
3583
- if (_inputRefs) {
3584
- const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3585
- if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3586
- }
3587
- return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3588
- } catch (e) {
3589
- const formatted = formatHeadlessConflictError(e);
3590
- if (formatted) return formatted;
3591
- throw e;
3592
- }
3593
- }
3594
-
3595
- const result = await client.sendEditorCommand("move_block", { fromFlat, toFlat, count }, 90000, _postId, _sessionId);
3596
- if (!result || result.timeout)
3597
- return timeoutResponse(name, client, args?.site);
3598
- if (!result.success)
3599
- return errorResponse(name, result.error, args?.site);
3600
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3601
- const _msg = moveMsg(result.moved);
3602
- if (_inputRefs) {
3603
- const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3604
- if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3605
- }
3606
- return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3607
- }
3608
-
3609
- // 既存モード(from/to トップレベル)
3610
- if (from === undefined || to === undefined) {
3611
- return { content: [{ type: "text", text: "❌ from と to の両方を指定してください" }], isError: true };
3548
+ // 同位置チェック
3549
+ if (fromFlat === toFlat || toFlat === fromFlat + count) {
3550
+ return { content: [{ type: "text", text: `✅ 移動不要(同じ位置)${_mt_mb}` }] };
3612
3551
  }
3613
3552
 
3614
3553
  if (mode === 'headless') {
3615
3554
  try {
3616
- const result = await client.headlessMove(postId, from, to, count);
3555
+ const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
3617
3556
  const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3618
- return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
3557
+ const _msg = moveMsg(result.moved);
3558
+ if (_inputRefs) {
3559
+ const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3560
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3561
+ }
3562
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3619
3563
  } catch (e) {
3620
3564
  const formatted = formatHeadlessConflictError(e);
3621
3565
  if (formatted) return formatted;
@@ -3623,13 +3567,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3623
3567
  }
3624
3568
  }
3625
3569
 
3626
- const result = await client.sendEditorCommand("move_block", { from, to, count }, 90000, _postId, _sessionId);
3570
+ const result = await client.sendEditorCommand("move_block", { fromFlat, toFlat, count }, 90000, _postId, _sessionId);
3627
3571
  if (!result || result.timeout)
3628
3572
  return timeoutResponse(name, client, args?.site);
3629
3573
  if (!result.success)
3630
3574
  return errorResponse(name, result.error, args?.site);
3631
3575
  const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3632
- return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
3576
+ const _msg = moveMsg(result.moved);
3577
+ if (_inputRefs) {
3578
+ const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3579
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3580
+ }
3581
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3633
3582
  }
3634
3583
 
3635
3584
  case "undo": {
@@ -3676,9 +3625,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3676
3625
  const _mt_dup = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
3677
3626
  const _siteName = siteName || args?.site || 'default';
3678
3627
 
3679
- // ref と index 排他チェック
3680
- if (args?.ref && args?.index !== undefined) {
3681
- return { content: [{ type: "text", text: "❌ ref index は同時に指定できません。" }], isError: true };
3628
+ // ref 必須チェック
3629
+ if (!args?.ref) {
3630
+ return { content: [{ type: "text", text: "❌ ref and snapshotId are required." }], isError: true };
3682
3631
  }
3683
3632
 
3684
3633
  // ref 解決 + revision チェック
@@ -3688,18 +3637,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3688
3637
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3689
3638
  });
3690
3639
  if (resolved.error) return resolved.error;
3691
- const _preState = resolved.currentState || null; // Phase 2: 差分計算用
3640
+ const _preState = resolved.currentState || null;
3692
3641
 
3693
- const index = resolved.index !== undefined ? resolved.index : args?.index;
3694
-
3695
- // Phase 3: 入力 ref を保持(index 経路は差分化しない)
3696
- const _inputRef = args?.ref || null;
3642
+ const index = resolved.index;
3643
+ const _inputRef = args.ref;
3697
3644
 
3698
3645
  if (mode === 'headless') {
3699
3646
  const _guard = await guardHeadlessConflict(postId, client, name);
3700
3647
  if (_guard) return _guard;
3701
3648
  if (index === undefined) {
3702
- return { content: [{ type: "text", text: "❌ Headless モードでは index の指定が必須です。" }], isError: true };
3649
+ return { content: [{ type: "text", text: "❌ ref resolution failed. Check snapshotId and ref." }], isError: true };
3703
3650
  }
3704
3651
  try {
3705
3652
  const result = await client.headlessDuplicate(postId, index);
@@ -3933,38 +3880,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3933
3880
  }
3934
3881
 
3935
3882
  case "insert_block": {
3936
- let { rawHTML, filePath, index, position } = (args || {});
3937
-
3938
- // parentIndex は廃止済み(v3.0.0 Phase 5)— position を使用
3939
- if (args?.parentIndex !== undefined) {
3940
- return { content: [{ type: "text", text: "❌ parentIndex は廃止されました。代わりに index + position ('before'/'after') を使用してください。" }], isError: true };
3941
- }
3883
+ let { rawHTML, filePath } = (args || {});
3942
3884
 
3943
3885
  // 排他チェック
3944
3886
  if (rawHTML && filePath) {
3945
3887
  return { content: [{ type: "text", text: "❌ rawHTML と filePath は同時に指定できません。どちらか一方を指定してください。" }], isError: true };
3946
3888
  }
3947
3889
 
3948
- // Phase 4b: beforeRef/afterRef と index/position の排他チェック
3949
3890
  const _hasRefPosition = args?.beforeRef !== undefined || args?.afterRef !== undefined;
3950
- const _hasIndexPosition = index !== undefined || position !== undefined;
3951
- if (_hasRefPosition && _hasIndexPosition) {
3952
- return { content: [{ type: "text", text: "❌ beforeRef/afterRef と index/position は同時に指定できません。" }], isError: true };
3953
- }
3954
3891
  if (args?.beforeRef && args?.afterRef) {
3955
3892
  return { content: [{ type: "text", text: "❌ beforeRef と afterRef は同時に指定できません。" }], isError: true };
3956
3893
  }
3957
3894
 
3958
- // position 指定時は index 必須(ref モードでないとき)
3959
- if (!_hasRefPosition && position !== undefined && index === undefined) {
3960
- return { content: [{ type: "text", text: "❌ position を指定する場合は index も指定してください。末尾追加は index を省略してください。" }], isError: true };
3961
- }
3962
-
3963
- // position バリデーション
3964
- if (position !== undefined && position !== "before" && position !== "after") {
3965
- return { content: [{ type: "text", text: `❌ position は 'before' または 'after' を指定してください: ${position}` }], isError: true };
3966
- }
3967
-
3968
3895
  // filePath → rawHTML 解決
3969
3896
  if (filePath) {
3970
3897
  try { rawHTML = readHTMLFromFile(filePath).html; }
@@ -3974,13 +3901,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3974
3901
  return { content: [{ type: "text", text: "❌ rawHTML または filePath を指定してください。Gutenberg HTML 形式のブロックマークアップが必要です。" }], isError: true };
3975
3902
  }
3976
3903
 
3977
- // index バリデーション(現行 Editor 互換: 整数 >= 0)— ref モードでないとき
3978
- if (!_hasRefPosition && index !== undefined && (!Number.isInteger(index) || index < 0)) {
3979
- return { content: [{ type: "text", text: `❌ Invalid index: ${index}` }], isError: true };
3980
- }
3981
-
3982
3904
  // update_blocks コードパスに委譲
3983
- // expectedRevision は全モードで委譲(ref/index/append 問わず)
3984
3905
  const delegatedArgs = {
3985
3906
  postId: args.postId,
3986
3907
  site: args.site,
@@ -3996,11 +3917,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3996
3917
  delegatedArgs.insert = { position: args.beforeRef ? 'before' : 'after' };
3997
3918
  delegatedArgs.snapshotId = args.snapshotId;
3998
3919
  delegatedArgs.appendToEnd = false;
3999
- } else if (index !== undefined) {
4000
- // 既存: index モード
4001
- delegatedArgs.target = { index };
4002
- delegatedArgs.insert = { position: position ?? 'before' };
4003
- delegatedArgs.appendToEnd = false;
4004
3920
  } else {
4005
3921
  // 末尾追加
4006
3922
  delegatedArgs.insert = { position: 'before' };
@@ -4172,9 +4088,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4172
4088
  return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
4173
4089
  }
4174
4090
 
4175
- // ref と index 排他チェック
4176
- if (args?.ref && args?.index !== undefined) {
4177
- return { content: [{ type: "text", text: "❌ ref index は同時に指定できません。" }], isError: true };
4091
+ // ref 必須チェック
4092
+ if (!args?.ref) {
4093
+ return { content: [{ type: "text", text: "❌ ref and snapshotId are required." }], isError: true };
4178
4094
  }
4179
4095
 
4180
4096
  // ref 解決 + revision チェック
@@ -4184,11 +4100,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4184
4100
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
4185
4101
  });
4186
4102
  if (resolved.error) return resolved.error;
4187
- const _preState = resolved.currentState || null; // Phase 2: 差分計算用
4103
+ const _preState = resolved.currentState || null;
4188
4104
 
4189
- const index = resolved.index !== undefined ? resolved.index : args?.index;
4105
+ const index = resolved.index;
4190
4106
  if (index === undefined) {
4191
- return { content: [{ type: "text", text: "❌ table_operations では index または ref のいずれかが必要です。" }], isError: true };
4107
+ return { content: [{ type: "text", text: "❌ ref resolution failed. Check snapshotId and ref." }], isError: true };
4192
4108
  }
4193
4109
 
4194
4110
  const tableParams = {
@@ -4424,7 +4340,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4424
4340
 
4425
4341
  default:
4426
4342
  throw new Error(`Unknown tool: ${name}`);
4343
+ } })();
4344
+ // 書き込みツールの成功時ログ
4345
+ if (_WRITE_TOOLS.has(name)) {
4346
+ const { newSnapshotId, diffIncluded } = _extractSnapshotFromResponse(_result);
4347
+ writeToolLog({
4348
+ tool: name,
4349
+ postId: args?.postId || null,
4350
+ targetType: _detectTargetType(args),
4351
+ snapshotIdProvided: !!args?.snapshotId,
4352
+ snapshotId: args?.snapshotId || null,
4353
+ operationType: _detectOperationType(name, args),
4354
+ responsePath: diffIncluded ? 'diff' : (newSnapshotId ? 'legacy' : 'none'),
4355
+ newSnapshotId,
4356
+ diffIncluded,
4357
+ isError: !!_result?.isError,
4358
+ durationMs: Date.now() - _toolLogStart,
4359
+ });
4427
4360
  }
4361
+ return _result;
4428
4362
  }
4429
4363
  catch (error) {
4430
4364
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -4435,6 +4369,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4435
4369
  content: errorMessage,
4436
4370
  site: args?.site || 'default',
4437
4371
  });
4372
+ writeToolLog({
4373
+ tool: name,
4374
+ postId: args?.postId || null,
4375
+ targetType: _detectTargetType(args),
4376
+ snapshotIdProvided: !!args?.snapshotId,
4377
+ error: errorMessage,
4378
+ });
4438
4379
  return {
4439
4380
  content: [{
4440
4381
  type: "text",
@@ -4445,6 +4386,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4445
4386
  }
4446
4387
  });
4447
4388
 
4389
+ // ツール呼び出しログ用: ターゲット指定方法を判定
4390
+ function _detectTargetType(args) {
4391
+ if (!args) return null;
4392
+ const t = args.target;
4393
+ if (t) {
4394
+ if (t.ref) return 'ref';
4395
+ if (t.refs) return 'refs';
4396
+ if (t.section) return 'section';
4397
+ if (t.heading) return 'heading';
4398
+ if (t.blockType) return 'blockType';
4399
+ if (t.selected) return 'selected';
4400
+ }
4401
+ if (args.ref || args.beforeRef || args.afterRef || args.fromRef) return 'ref';
4402
+ if (args.refs) return 'refs';
4403
+ return null;
4404
+ }
4405
+
4406
+ // ツール呼び出しログ用: 操作タイプを判定
4407
+ function _detectOperationType(toolName, args) {
4408
+ if (toolName === 'delete_block') return 'delete';
4409
+ if (toolName === 'move_block') return 'move';
4410
+ if (toolName === 'duplicate_block') return 'duplicate';
4411
+ if (toolName === 'insert_block') return 'insert';
4412
+ if (toolName === 'table_operations') return `table_${args?.action || 'unknown'}`;
4413
+ // update_blocks
4414
+ if (args?.operations) return 'batch';
4415
+ if (args?.insert || args?._fromInsertBlock) return 'insert';
4416
+ if (args?.newHTML) return 'newHTML';
4417
+ if (args?.replacements?.length > 0) return 'replacements';
4418
+ if (args?.attributeUpdates) return 'attributeUpdates';
4419
+ return 'unknown';
4420
+ }
4421
+
4422
+ // ツール呼び出しログ用: レスポンスから snapshotId を抽出
4423
+ function _extractSnapshotFromResponse(response) {
4424
+ if (!response?.content?.[0]?.text) return { newSnapshotId: null, diffIncluded: false };
4425
+ const text = response.content[0].text;
4426
+ const snapMatch = text.match(/snapshotId: (snap_[a-f0-9]+)/);
4427
+ const diffIncluded = /(?:updated|deleted|inserted|moved|duplicated|expanded|compressed): \[/.test(text);
4428
+ return { newSnapshotId: snapMatch ? snapMatch[1] : null, diffIncluded };
4429
+ }
4430
+
4448
4431
  // サーバー起動
4449
4432
  async function main() {
4450
4433
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "friday-mcp-v2",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "WordPress MCP Server for Claude Code - REST API direct communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp-server.js",