friday-mcp-v2 2.0.5 → 2.1.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/dist/mcp-server.js +1060 -464
- package/dist/wordpress-api.js +53 -19
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -10,61 +10,553 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
11
11
|
import { FridayWPClient, ConnectionRegistry } from "./wordpress-api.js";
|
|
12
12
|
import fetch from "node-fetch";
|
|
13
|
+
import { readFileSync, statSync, realpathSync } from "node:fs";
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { resolve as resolvePath, sep, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
// package.json からバージョンを取得
|
|
18
|
+
const __pkg_dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const SERVER_VERSION = (() => {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(resolvePath(__pkg_dirname, '..', 'package.json'), 'utf-8'));
|
|
22
|
+
return pkg.version || '0.0.0';
|
|
23
|
+
} catch { return '0.0.0'; }
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
// .env 読み込み(FRIDAY_WORKSPACE 配下の .env を自動ロード)
|
|
27
|
+
// .mcp.json の env が優先され、.env は未設定キーの補完のみ
|
|
28
|
+
const _envWorkspace = process.env.FRIDAY_WORKSPACE;
|
|
29
|
+
if (_envWorkspace) {
|
|
30
|
+
try {
|
|
31
|
+
const _envPath = resolvePath(_envWorkspace, '.env');
|
|
32
|
+
const _envContent = readFileSync(_envPath, 'utf-8');
|
|
33
|
+
for (const _line of _envContent.split('\n')) {
|
|
34
|
+
const _trimmed = _line.trim();
|
|
35
|
+
if (!_trimmed || _trimmed.startsWith('#')) continue;
|
|
36
|
+
const _eqIdx = _trimmed.indexOf('=');
|
|
37
|
+
if (_eqIdx === -1) continue;
|
|
38
|
+
const _key = _trimmed.slice(0, _eqIdx).trim();
|
|
39
|
+
const _val = _trimmed.slice(_eqIdx + 1).trim();
|
|
40
|
+
if (!process.env[_key]) {
|
|
41
|
+
process.env[_key] = _val;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (_e) {
|
|
45
|
+
// .env がなくても起動は続行
|
|
46
|
+
}
|
|
47
|
+
}
|
|
13
48
|
|
|
14
49
|
const registry = new ConnectionRegistry();
|
|
15
50
|
|
|
51
|
+
// Sticky site キャッシュ: 自動検出した接続先を短時間記憶
|
|
52
|
+
const statusCache = {
|
|
53
|
+
_entries: new Map(),
|
|
54
|
+
_lastSite: null,
|
|
55
|
+
TTL: 15_000,
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 指定サイトのキャッシュを取得(TTL 切れなら自動破棄)
|
|
59
|
+
* @param {string} site
|
|
60
|
+
* @returns {{ site: string, client: FridayWPClient, postId: number, checkedAt: number } | null}
|
|
61
|
+
*/
|
|
62
|
+
get(site) {
|
|
63
|
+
const entry = this._entries.get(site);
|
|
64
|
+
if (!entry) return null;
|
|
65
|
+
if ((Date.now() - entry.checkedAt) >= this.TTL) {
|
|
66
|
+
this._entries.delete(site);
|
|
67
|
+
if (this._lastSite === site) this._lastSite = null;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return { site, ...entry };
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 最後に set されたサイトのキャッシュを返す(sticky 参照用)
|
|
75
|
+
* 現行の isValid() + 単一スロット参照と同じセマンティクスを維持する。
|
|
76
|
+
* Map 走査順に依存せず、明示的に _lastSite を追跡することで
|
|
77
|
+
* 既存の検出限界(キャッシュ/default ヒット時は複数 editor 未検出)を含めて維持する。
|
|
78
|
+
*/
|
|
79
|
+
getLast() {
|
|
80
|
+
if (!this._lastSite) return null;
|
|
81
|
+
return this.get(this._lastSite);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
set(site, client, postId) {
|
|
85
|
+
this._entries.set(site, { client, postId, checkedAt: Date.now() });
|
|
86
|
+
this._lastSite = site;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} [site] 指定時はそのサイトのみ破棄。省略時は全破棄。
|
|
91
|
+
*/
|
|
92
|
+
clear(site) {
|
|
93
|
+
if (site) {
|
|
94
|
+
this._entries.delete(site);
|
|
95
|
+
if (this._lastSite === site) this._lastSite = null;
|
|
96
|
+
} else {
|
|
97
|
+
this._entries.clear();
|
|
98
|
+
this._lastSite = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
16
103
|
// ヘルパー関数: 正規表現用に文字列をエスケープ
|
|
17
104
|
function escapeRegExp(string) {
|
|
18
105
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
106
|
}
|
|
20
107
|
|
|
21
108
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
109
|
+
* OS のデフォルトブラウザで URL を開く(execFile でシェル非経由)
|
|
24
110
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
111
|
+
function openInBrowser(url) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
let cmd, args;
|
|
114
|
+
switch (process.platform) {
|
|
115
|
+
case 'win32':
|
|
116
|
+
cmd = 'rundll32';
|
|
117
|
+
args = ['url.dll,FileProtocolHandler', url];
|
|
118
|
+
break;
|
|
119
|
+
case 'darwin':
|
|
120
|
+
cmd = 'open';
|
|
121
|
+
args = [url];
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
cmd = 'xdg-open';
|
|
125
|
+
args = [url];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
execFile(cmd, args, (error) => {
|
|
129
|
+
if (error) reject(error);
|
|
130
|
+
else resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* URL バリデーション(http/https のみ許可)
|
|
137
|
+
*/
|
|
138
|
+
function isValidHttpUrl(url) {
|
|
27
139
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
140
|
+
const parsed = new URL(url);
|
|
141
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* filePath からHTMLを読み込む共通ユーティリティ
|
|
149
|
+
* FRIDAY_WORKSPACE 環境変数で許可ディレクトリを制限
|
|
150
|
+
*/
|
|
151
|
+
function readHTMLFromFile(filePath, maxSizeBytes = 2 * 1024 * 1024) {
|
|
152
|
+
const resolved = resolvePath(filePath);
|
|
153
|
+
|
|
154
|
+
const workspace = process.env.FRIDAY_WORKSPACE;
|
|
155
|
+
if (!workspace) {
|
|
156
|
+
throw new Error('FRIDAY_WORKSPACE 環境変数が未設定です。.mcp.json の env で設定してください。');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// セキュリティ: 実体パスで比較(シンボリックリンク/ジャンクション経由の迂回を防止)
|
|
160
|
+
let realPath;
|
|
161
|
+
try { realPath = realpathSync(resolved); }
|
|
162
|
+
catch (e) {
|
|
163
|
+
if (e.code === 'ENOENT') throw new Error(`ファイルが見つかりません: ${resolved}`);
|
|
164
|
+
throw new Error(`ファイルアクセスエラー: ${resolved} (${e.message})`);
|
|
165
|
+
}
|
|
166
|
+
let realRoot;
|
|
167
|
+
try { realRoot = realpathSync(resolvePath(workspace)); }
|
|
168
|
+
catch (e) {
|
|
169
|
+
throw new Error(`FRIDAY_WORKSPACE のパスが無効です: ${workspace} (${e.message})`);
|
|
170
|
+
}
|
|
171
|
+
if (!realPath.startsWith(realRoot + sep) && realPath !== realRoot) {
|
|
172
|
+
throw new Error(`許可されたディレクトリ外です: ${realPath} (FRIDAY_WORKSPACE: ${realRoot})`);
|
|
31
173
|
}
|
|
32
174
|
|
|
175
|
+
let stat;
|
|
176
|
+
try { stat = statSync(realPath); }
|
|
177
|
+
catch (e) {
|
|
178
|
+
throw new Error(`ファイルアクセスエラー: ${realPath} (${e.message})`);
|
|
179
|
+
}
|
|
180
|
+
if (stat.size > maxSizeBytes) {
|
|
181
|
+
throw new Error(`ファイルサイズ超過: ${(stat.size / 1048576).toFixed(1)}MB (上限: ${(maxSizeBytes / 1048576).toFixed(1)}MB)`);
|
|
182
|
+
}
|
|
183
|
+
let html;
|
|
184
|
+
try { html = readFileSync(realPath, 'utf-8'); }
|
|
185
|
+
catch (e) { throw new Error(`ファイル読み込み失敗: ${realPath} (${e.message})`); }
|
|
186
|
+
if (!html.trim()) throw new Error(`ファイルが空です: ${realPath}`);
|
|
187
|
+
return { html };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 単一クライアントの Editor/Headless 判定
|
|
192
|
+
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
193
|
+
*/
|
|
194
|
+
async function checkSingleClient(client, args) {
|
|
33
195
|
try {
|
|
34
|
-
const
|
|
196
|
+
const rawPostId = args?.postId;
|
|
197
|
+
const numPostId = rawPostId != null ? Number(rawPostId) : null;
|
|
198
|
+
const isNumeric = numPostId != null && Number.isFinite(numPostId) && numPostId > 0;
|
|
199
|
+
// getStatus の checkPostId は数値のみ意味がある。スラッグ/空文字時は null
|
|
200
|
+
const status = await client.getStatus(isNumeric ? numPostId : null);
|
|
201
|
+
|
|
35
202
|
if (status.editorConnected && status.postId) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
203
|
+
if (rawPostId != null) {
|
|
204
|
+
if (isNumeric) {
|
|
205
|
+
if (numPostId === Number(status.postId) || status.checkedEditorConnected) {
|
|
206
|
+
return {
|
|
207
|
+
mode: 'error',
|
|
208
|
+
errorCode: 'editor_conflict',
|
|
209
|
+
client,
|
|
210
|
+
message: `postId ${numPostId} はエディタで開かれています。postId を省略してエディタ経由で操作するか、エディタを閉じてから Headless モードを使用してください。`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return { mode: 'headless', postId: numPostId, client };
|
|
214
|
+
}
|
|
215
|
+
// スラッグ指定 → headless(ID 解決 + 競合チェックは resolvePostId で行う)
|
|
216
|
+
return { mode: 'headless', postId: null, client };
|
|
39
217
|
}
|
|
40
218
|
return { mode: 'editor', postId: status.postId, client };
|
|
41
219
|
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
return {
|
|
220
|
+
|
|
221
|
+
if (isNumeric && status.checkedEditorConnected) {
|
|
222
|
+
return {
|
|
223
|
+
mode: 'error',
|
|
224
|
+
errorCode: 'editor_conflict',
|
|
225
|
+
client,
|
|
226
|
+
message: `postId ${numPostId} はエディタで開かれています。postId を省略してエディタ経由で操作するか、エディタを閉じてから Headless モードを使用してください。`
|
|
227
|
+
};
|
|
45
228
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
229
|
+
|
|
230
|
+
if (rawPostId == null) {
|
|
231
|
+
return { mode: 'error', message: 'エディタ未接続' };
|
|
232
|
+
}
|
|
233
|
+
if (isNumeric) {
|
|
234
|
+
return { mode: 'headless', postId: numPostId, client };
|
|
51
235
|
}
|
|
236
|
+
// スラッグ指定
|
|
237
|
+
return { mode: 'headless', postId: null, client };
|
|
238
|
+
} catch (e) {
|
|
52
239
|
return { mode: 'error', message: `接続エラー: ${e.message}` };
|
|
53
240
|
}
|
|
54
241
|
}
|
|
55
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Editor/Headless モード判定ヘルパー(複数接続自動検出対応)
|
|
245
|
+
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
246
|
+
*/
|
|
247
|
+
async function resolveMode(args) {
|
|
248
|
+
// site 明示指定
|
|
249
|
+
if (args?.site) {
|
|
250
|
+
let client;
|
|
251
|
+
try { client = registry.get(args.site); }
|
|
252
|
+
catch (e) { return { mode: 'error', message: e.message }; }
|
|
253
|
+
const result = await checkSingleClient(client, args);
|
|
254
|
+
return { ...result, siteName: args.site };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const allConns = registry.getAll();
|
|
258
|
+
const isMultiSite = allConns.length > 1;
|
|
259
|
+
|
|
260
|
+
// 単一接続 → 従来通り
|
|
261
|
+
if (!isMultiSite) {
|
|
262
|
+
const result = await checkSingleClient(registry.get(), args);
|
|
263
|
+
return { ...result, siteName: allConns[0].name };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 複数接続 + sticky キャッシュ有効
|
|
267
|
+
const cached = statusCache.getLast();
|
|
268
|
+
if (cached) {
|
|
269
|
+
const cachedResult = await checkSingleClient(cached.client, args);
|
|
270
|
+
if (cachedResult.mode === 'editor') {
|
|
271
|
+
return { ...cachedResult, siteName: cached.site };
|
|
272
|
+
}
|
|
273
|
+
statusCache.clear(cached.site);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// default を先に確認
|
|
277
|
+
const defaultClient = registry.get();
|
|
278
|
+
const defaultName = allConns.find(c => c.client === defaultClient)?.name || 'default';
|
|
279
|
+
const defaultResult = await checkSingleClient(defaultClient, args);
|
|
280
|
+
if (defaultResult.mode === 'editor') {
|
|
281
|
+
statusCache.set(defaultName, defaultClient, defaultResult.postId);
|
|
282
|
+
return { ...defaultResult, siteName: defaultName };
|
|
283
|
+
}
|
|
284
|
+
// defaultResult.mode === 'headless' の場合:
|
|
285
|
+
// 他の接続にエディタが接続中の可能性があるため、並列スキャンに進む
|
|
286
|
+
|
|
287
|
+
// 全接続並列探索(各接続 5秒タイムアウト付き)
|
|
288
|
+
const SCAN_TIMEOUT = 5000;
|
|
289
|
+
const statusResults = await Promise.allSettled(
|
|
290
|
+
allConns.map(async ({ name, client }) => {
|
|
291
|
+
const status = await Promise.race([
|
|
292
|
+
client.getStatus(),
|
|
293
|
+
new Promise((_, reject) =>
|
|
294
|
+
setTimeout(() => reject(new Error('timeout')), SCAN_TIMEOUT)
|
|
295
|
+
)
|
|
296
|
+
]);
|
|
297
|
+
return { name, client, status };
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const connected = statusResults
|
|
302
|
+
.filter(r => r.status === 'fulfilled')
|
|
303
|
+
.map(r => r.value)
|
|
304
|
+
.filter(r => r.status.editorConnected && r.status.postId);
|
|
305
|
+
|
|
306
|
+
if (connected.length === 1) {
|
|
307
|
+
const { name, client, status } = connected[0];
|
|
308
|
+
if (args?.postId != null) {
|
|
309
|
+
const numPid = Number(args.postId);
|
|
310
|
+
const isNum = Number.isFinite(numPid) && numPid > 0;
|
|
311
|
+
if (!isNum || numPid !== Number(status.postId)) {
|
|
312
|
+
// postId 不一致またはスラッグ: エディタは別記事を開いている → site 必須エラー
|
|
313
|
+
const connNames = allConns.map(c => c.name).join(', ');
|
|
314
|
+
return {
|
|
315
|
+
mode: 'error',
|
|
316
|
+
message: `複数サイトが登録されています。Headless モードでは site の指定が必須です。\n利用可能: ${connNames}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
statusCache.set(name, client, status.postId);
|
|
322
|
+
return { mode: 'editor', postId: status.postId, client, siteName: name };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (connected.length > 1) {
|
|
326
|
+
const list = connected.map(c =>
|
|
327
|
+
` ${c.name}: postId ${c.status.postId} (${c.client.wpUrl})`
|
|
328
|
+
).join('\n');
|
|
329
|
+
return {
|
|
330
|
+
mode: 'error',
|
|
331
|
+
message: `複数のエディタが接続中です。site パラメータで接続先を指定してください。\n\n接続中:\n${list}\n\n例: get_article_structure({ site: "${connected[0].name}" })`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// どれも未接続 + postId 指定あり → Headless だが site 未指定
|
|
336
|
+
if (args?.postId != null) {
|
|
337
|
+
const connNames = allConns.map(c => c.name).join(', ');
|
|
338
|
+
return {
|
|
339
|
+
mode: 'error',
|
|
340
|
+
message: `複数サイトが登録されています。Headless モードでは site の指定が必須です。\n利用可能: ${connNames}`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// どれも未接続 + postId なし
|
|
345
|
+
return {
|
|
346
|
+
mode: 'error',
|
|
347
|
+
message: 'エディタ未接続です。postId を指定して Headless モードを使用するか、エディタで記事を開いてください。\n接続一覧の確認: list_connections()'
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* postId を解決する(数値はそのまま、文字列は slug として検索)
|
|
353
|
+
* slug 解決後にエディタ競合チェックも実施
|
|
354
|
+
*/
|
|
355
|
+
async function resolvePostId(rawPostId, client, { skipConflictCheck = false } = {}) {
|
|
356
|
+
if (rawPostId == null) return { postId: null };
|
|
357
|
+
|
|
358
|
+
// 数値判定("" → 0 を除外するため num > 0)
|
|
359
|
+
const num = Number(rawPostId);
|
|
360
|
+
if (Number.isFinite(num) && num > 0) return { postId: num };
|
|
361
|
+
|
|
362
|
+
// 空文字チェック(Number("") === 0 で上を通過するため、ここで明示エラー)
|
|
363
|
+
let slug = String(rawPostId).trim();
|
|
364
|
+
if (!slug) return { error: '❌ postId が空です。' };
|
|
365
|
+
|
|
366
|
+
// URL → スラッグ抽出(query/hash を除去)
|
|
367
|
+
if (slug.includes('/')) {
|
|
368
|
+
try {
|
|
369
|
+
const url = new URL(slug);
|
|
370
|
+
slug = url.pathname.replace(/\/+$/, '').split('/').pop() || '';
|
|
371
|
+
} catch {
|
|
372
|
+
// URL パース失敗 → 相対パスとして末尾を取る
|
|
373
|
+
slug = slug.split('?')[0].split('#')[0].replace(/\/+$/, '').split('/').pop() || '';
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!slug) return { error: '❌ URL からスラッグを抽出できませんでした。' };
|
|
377
|
+
|
|
378
|
+
// slug → ID 解決
|
|
379
|
+
const { data: posts } = await client.listPosts({ slug, status: 'any' });
|
|
380
|
+
if (!posts || posts.length === 0) {
|
|
381
|
+
return { error: `❌ スラッグ "${slug}" に一致する投稿が見つかりません。` };
|
|
382
|
+
}
|
|
383
|
+
const resolvedId = posts[0].id;
|
|
384
|
+
|
|
385
|
+
// ★ エディタ競合チェック(slug 経由でも既存と同等のガードを維持)
|
|
386
|
+
if (!skipConflictCheck) {
|
|
387
|
+
const status = await client.getStatus(resolvedId);
|
|
388
|
+
if (status.editorConnected &&
|
|
389
|
+
(resolvedId === Number(status.postId) || status.checkedEditorConnected)) {
|
|
390
|
+
return {
|
|
391
|
+
error: `❌ postId ${resolvedId}(${slug})はエディタで開かれています。postId を省略してエディタ経由で操作するか、エディタを閉じてから Headless モードを使用してください。`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { postId: resolvedId };
|
|
397
|
+
}
|
|
398
|
+
|
|
56
399
|
// postId パラメータ定義(Headless 対応ツール共通)
|
|
57
400
|
const postIdParam = {
|
|
58
|
-
|
|
59
|
-
|
|
401
|
+
oneOf: [
|
|
402
|
+
{ type: "number" },
|
|
403
|
+
{ type: "string" }
|
|
404
|
+
],
|
|
405
|
+
description: "投稿ID(数値)またはスラッグ(文字列, URLパス末尾。例: 'madear')。URLも可(自動抽出)。スラッグ指定時は自動的にIDを解決。エディタ未接続時は必須。",
|
|
60
406
|
};
|
|
61
407
|
|
|
62
408
|
// site パラメータ定義(マルチ接続共通)
|
|
63
409
|
const siteParam = {
|
|
64
410
|
type: "string",
|
|
65
|
-
description: "
|
|
411
|
+
description: "接続名(複数サイト/ユーザー設定時に指定。省略でデフォルト接続。Headless + 複数サイト時は必須)",
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// ========================================
|
|
415
|
+
// ターゲット / インサート 共通スキーマ & ヘルパー
|
|
416
|
+
// ========================================
|
|
417
|
+
|
|
418
|
+
const targetSchema = {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
selected: {
|
|
422
|
+
type: "boolean",
|
|
423
|
+
description: "Set true to target the block currently selected by the user in WordPress editor. Cannot be used in Headless mode.",
|
|
424
|
+
},
|
|
425
|
+
index: {
|
|
426
|
+
type: "number",
|
|
427
|
+
description: "Single block index (0-based flattened position including nested blocks).",
|
|
428
|
+
},
|
|
429
|
+
indices: {
|
|
430
|
+
type: "array",
|
|
431
|
+
items: { type: "number" },
|
|
432
|
+
description: "Multiple block indices (0-based flattened).",
|
|
433
|
+
},
|
|
434
|
+
range: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
start: { type: "number", description: "Start index (inclusive)." },
|
|
438
|
+
end: { type: "number", description: "End index (inclusive)." },
|
|
439
|
+
},
|
|
440
|
+
required: ["start", "end"],
|
|
441
|
+
description: "Contiguous range of block indices.",
|
|
442
|
+
},
|
|
443
|
+
section: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "Target section by heading text (partial match). Combines with blockType/contains.",
|
|
446
|
+
},
|
|
447
|
+
blockType: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "Filter by block type (e.g. core/table). Combines with section/contains.",
|
|
450
|
+
},
|
|
451
|
+
nth: {
|
|
452
|
+
type: "number",
|
|
453
|
+
description: "N-th block of the specified blockType (0-based). Use with blockType.",
|
|
454
|
+
},
|
|
455
|
+
contains: {
|
|
456
|
+
type: "string",
|
|
457
|
+
description: "Target blocks containing this text.",
|
|
458
|
+
},
|
|
459
|
+
heading: {
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {
|
|
462
|
+
level: { type: "number", description: "Heading level (2, 3, 4)." },
|
|
463
|
+
contains: { type: "string", description: "Heading text to match (partial)." },
|
|
464
|
+
},
|
|
465
|
+
required: ["level", "contains"],
|
|
466
|
+
description: "Section selection by heading level and text. Selects from the heading to the next same-level heading.",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
description: "Block targeting criteria. Use one primary selector (selected/index/indices/range/heading) optionally combined with filters (blockType/contains/section).",
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const targetSchemaNoSelected = {
|
|
473
|
+
...targetSchema,
|
|
474
|
+
properties: { ...targetSchema.properties },
|
|
475
|
+
};
|
|
476
|
+
delete targetSchemaNoSelected.properties.selected;
|
|
477
|
+
targetSchemaNoSelected.description = "Block targeting criteria. Use one primary selector (index/range/heading) optionally combined with filters (blockType/contains/section).";
|
|
478
|
+
|
|
479
|
+
const insertSchema = {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
position: {
|
|
483
|
+
type: ["number", "string"],
|
|
484
|
+
description: "Insert position. 'before', 'after' (default), or block index number. Only with newHTML.",
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
description: "Insert mode: keep existing blocks and add newHTML as new blocks. Presence of this parameter enables insert mode.",
|
|
66
488
|
};
|
|
67
489
|
|
|
490
|
+
/**
|
|
491
|
+
* 新形式の target オブジェクトを下流用のフラット形式に変換する。
|
|
492
|
+
* @param {object} target - 新形式 target (e.g. { selected: true }, { index: 3 })
|
|
493
|
+
* @returns {object} フラット形式 (e.g. { target: "selected" }, { index: 3 })
|
|
494
|
+
* @throws {Error} primary selector が複数指定された場合
|
|
495
|
+
*/
|
|
496
|
+
function normalizeTarget(target) {
|
|
497
|
+
if (!target || typeof target !== "object") return {};
|
|
498
|
+
|
|
499
|
+
// --- 排他バリデーション ---
|
|
500
|
+
const primaries = [
|
|
501
|
+
target.selected && 'selected',
|
|
502
|
+
target.index !== undefined && 'index',
|
|
503
|
+
target.indices && 'indices',
|
|
504
|
+
target.range && 'range',
|
|
505
|
+
target.heading && 'heading',
|
|
506
|
+
].filter(Boolean);
|
|
507
|
+
if (primaries.length > 1) {
|
|
508
|
+
throw new Error(`target に複数の primary selector が指定されています: ${primaries.join(', ')}。1つだけ指定してください。`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const result = {};
|
|
512
|
+
if (target.selected) result.target = "selected";
|
|
513
|
+
if (target.index !== undefined) result.index = target.index;
|
|
514
|
+
if (target.indices) result.indices = target.indices;
|
|
515
|
+
if (target.range) {
|
|
516
|
+
const s = target.range.start;
|
|
517
|
+
const e = target.range.end;
|
|
518
|
+
if (s === undefined || s === null || e === undefined || e === null ||
|
|
519
|
+
typeof s !== "number" || typeof e !== "number" ||
|
|
520
|
+
!Number.isInteger(s) || !Number.isInteger(e) || s < 0 || e < 0) {
|
|
521
|
+
throw new Error("range.start と range.end は 0 以上の整数で両方指定してください。");
|
|
522
|
+
}
|
|
523
|
+
result.startIndex = s;
|
|
524
|
+
result.endIndex = e;
|
|
525
|
+
}
|
|
526
|
+
if (target.section) result.section = target.section;
|
|
527
|
+
if (target.blockType) result.blockType = target.blockType;
|
|
528
|
+
if (target.nth !== undefined) result.typeIndex = target.nth;
|
|
529
|
+
if (target.contains) result.contains = target.contains;
|
|
530
|
+
if (target.heading) {
|
|
531
|
+
const lv = target.heading.level;
|
|
532
|
+
const ct = target.heading.contains;
|
|
533
|
+
if (lv === undefined || typeof lv !== "number" || !ct || typeof ct !== "string") {
|
|
534
|
+
throw new Error("heading には level(数値)と contains(非空文字列)の両方が必要です。");
|
|
535
|
+
}
|
|
536
|
+
result.headingLevel = lv;
|
|
537
|
+
result.headingContains = ct;
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* 新形式の insert オブジェクトを下流用のフラット形式に変換する。
|
|
544
|
+
* @param {object} insert - 新形式 insert (e.g. { position: "after" })
|
|
545
|
+
* @returns {object} フラット形式 { insertOnly, insertPosition }
|
|
546
|
+
* @throws {Error} position の値が不正な場合
|
|
547
|
+
*/
|
|
548
|
+
function normalizeInsert(insert) {
|
|
549
|
+
if (!insert) return { insertOnly: false };
|
|
550
|
+
const pos = insert.position ?? "after";
|
|
551
|
+
if (pos !== "before" && pos !== "after" && typeof pos !== "number") {
|
|
552
|
+
throw new Error(`insert.position の値が不正です: ${pos}。'before', 'after', または数値を指定してください。`);
|
|
553
|
+
}
|
|
554
|
+
if (typeof pos === "number" && (!Number.isInteger(pos) || pos < 0)) {
|
|
555
|
+
throw new Error(`insert.position は 0 以上の整数を指定してください: ${pos}`);
|
|
556
|
+
}
|
|
557
|
+
return { insertOnly: true, insertPosition: pos };
|
|
558
|
+
}
|
|
559
|
+
|
|
68
560
|
// フィードバック送信ヘルパー
|
|
69
561
|
const FEEDBACK_URL = process.env.FRIDAY_FEEDBACK_URL || '';
|
|
70
562
|
async function sendFeedback(data) {
|
|
@@ -73,7 +565,7 @@ async function sendFeedback(data) {
|
|
|
73
565
|
await fetch(FEEDBACK_URL, {
|
|
74
566
|
method: 'POST',
|
|
75
567
|
headers: { 'Content-Type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ ...data, version:
|
|
568
|
+
body: JSON.stringify({ ...data, version: SERVER_VERSION }),
|
|
77
569
|
});
|
|
78
570
|
} catch (_) {
|
|
79
571
|
// フィードバック送信失敗は無視(本体の動作に影響させない)
|
|
@@ -104,7 +596,7 @@ function timeoutResponse(toolName, client, site) {
|
|
|
104
596
|
// MCPサーバーの定義
|
|
105
597
|
const server = new Server({
|
|
106
598
|
name: "friday-mcp-v2",
|
|
107
|
-
version:
|
|
599
|
+
version: SERVER_VERSION,
|
|
108
600
|
}, {
|
|
109
601
|
capabilities: {
|
|
110
602
|
tools: {},
|
|
@@ -163,39 +655,9 @@ const tools = [
|
|
|
163
655
|
type: "object",
|
|
164
656
|
properties: {
|
|
165
657
|
site: siteParam,
|
|
166
|
-
|
|
167
|
-
type: "number",
|
|
168
|
-
description: "Block index (0-based, flattened including nested blocks)",
|
|
169
|
-
},
|
|
170
|
-
blockType: {
|
|
171
|
-
type: "string",
|
|
172
|
-
description: "Block type (e.g. core/table)",
|
|
173
|
-
},
|
|
174
|
-
typeIndex: {
|
|
175
|
-
type: "number",
|
|
176
|
-
description: "N-th block of the type (0-based)",
|
|
177
|
-
},
|
|
178
|
-
contains: {
|
|
179
|
-
type: "string",
|
|
180
|
-
description: "Search text in block content",
|
|
181
|
-
},
|
|
182
|
-
startIndex: {
|
|
183
|
-
type: "number",
|
|
184
|
-
description: "Range selection start",
|
|
185
|
-
},
|
|
186
|
-
endIndex: {
|
|
187
|
-
type: "number",
|
|
188
|
-
description: "Range selection end",
|
|
189
|
-
},
|
|
190
|
-
headingLevel: {
|
|
191
|
-
type: "number",
|
|
192
|
-
description: "Heading level for section selection (2, 3, 4)",
|
|
193
|
-
},
|
|
194
|
-
headingContains: {
|
|
195
|
-
type: "string",
|
|
196
|
-
description: "Heading text for section selection",
|
|
197
|
-
},
|
|
658
|
+
target: targetSchemaNoSelected,
|
|
198
659
|
},
|
|
660
|
+
required: ["target"],
|
|
199
661
|
},
|
|
200
662
|
},
|
|
201
663
|
{
|
|
@@ -214,7 +676,7 @@ const tools = [
|
|
|
214
676
|
},
|
|
215
677
|
{
|
|
216
678
|
name: "move_block",
|
|
217
|
-
description: "Move
|
|
679
|
+
description: "Move block(s) to a different position. Use from/to for top-level blocks, or fromFlat/toFlat for any block (including nested). Use count to move consecutive blocks.",
|
|
218
680
|
inputSchema: {
|
|
219
681
|
type: "object",
|
|
220
682
|
properties: {
|
|
@@ -224,6 +686,7 @@ const tools = [
|
|
|
224
686
|
to: { type: "number", description: "Target position (0-based, can be length for end)" },
|
|
225
687
|
fromFlat: { type: "number", description: "Source flattened block index (0-based, supports nested blocks)" },
|
|
226
688
|
toFlat: { type: "number", description: "Target flattened position (pre-move basis, can be blockCount for end)" },
|
|
689
|
+
count: { type: "integer", description: "Number of consecutive blocks to move (default: 1)" },
|
|
227
690
|
},
|
|
228
691
|
},
|
|
229
692
|
},
|
|
@@ -303,13 +766,14 @@ const tools = [
|
|
|
303
766
|
},
|
|
304
767
|
{
|
|
305
768
|
name: "list_posts",
|
|
306
|
-
description: "Search and list WordPress posts. Use to find postId for
|
|
769
|
+
description: "Search and list WordPress posts. Use to find postId for other tools. For exact lookup, use 'slug' (from URL path e.g. 'my-post' from example.com/my-post/); slug auto-sets status='any' and returns exactly 1 result. For broad search, use 'search' keyword. slug and search are mutually exclusive.",
|
|
307
770
|
inputSchema: {
|
|
308
771
|
type: "object",
|
|
309
772
|
properties: {
|
|
310
773
|
site: siteParam,
|
|
311
|
-
|
|
312
|
-
|
|
774
|
+
slug: { type: "string", description: "Post slug for exact match (from URL path e.g. 'comparison' from example.com/comparison). Mutually exclusive with search." },
|
|
775
|
+
search: { type: "string", description: "Search keyword (title/content). Mutually exclusive with slug." },
|
|
776
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private", "any"], description: "Filter by status (default: publish; auto-set to 'any' when slug is specified)" },
|
|
313
777
|
categories: { type: "array", items: { type: "number" }, description: "Category IDs to filter by" },
|
|
314
778
|
tags: { type: "array", items: { type: "number" }, description: "Tag IDs to filter by" },
|
|
315
779
|
per_page: { type: "integer", description: "Results per page (1-100, default: 10)", minimum: 1, maximum: 100 },
|
|
@@ -339,17 +803,15 @@ const tools = [
|
|
|
339
803
|
},
|
|
340
804
|
{
|
|
341
805
|
name: "insert_block",
|
|
342
|
-
description: "Insert
|
|
806
|
+
description: "Insert Gutenberg HTML at the specified position. If index is omitted, appends to end. HTML can be provided directly via rawHTML or loaded from a local file via filePath.",
|
|
343
807
|
inputSchema: {
|
|
344
808
|
type: "object",
|
|
345
809
|
properties: {
|
|
346
810
|
postId: postIdParam,
|
|
347
811
|
site: siteParam,
|
|
348
|
-
|
|
349
|
-
|
|
812
|
+
rawHTML: { type: "string", description: "Gutenberg markup HTML to insert. Mutually exclusive with filePath." },
|
|
813
|
+
filePath: { type: "string", description: "Absolute path to a local file containing Gutenberg HTML. Read as UTF-8. Mutually exclusive with rawHTML." },
|
|
350
814
|
index: { type: "number", description: "Insert position (0-based flattened index). If omitted, appends to end." },
|
|
351
|
-
blocks: { type: "array", items: { type: "object", properties: { blockType: { type: "string" }, content: { type: "string" } }, required: ["blockType", "content"] }, description: "Multiple blocks to insert. Cannot be used with blockType/content." },
|
|
352
|
-
rawHTML: { type: "string", description: "Gutenberg markup HTML to insert directly. Cannot be used with blockType/content or blocks." },
|
|
353
815
|
},
|
|
354
816
|
},
|
|
355
817
|
},
|
|
@@ -371,49 +833,9 @@ const tools = [
|
|
|
371
833
|
properties: {
|
|
372
834
|
postId: postIdParam,
|
|
373
835
|
site: siteParam,
|
|
374
|
-
target:
|
|
375
|
-
type: "string",
|
|
376
|
-
enum: ["selected"],
|
|
377
|
-
description: "Use 'selected' to target the block currently selected by the user in WordPress editor.",
|
|
378
|
-
},
|
|
379
|
-
index: {
|
|
380
|
-
type: "number",
|
|
381
|
-
description: "Single block index (0-based flattened).",
|
|
382
|
-
},
|
|
383
|
-
indices: {
|
|
384
|
-
type: "array",
|
|
385
|
-
items: { type: "number" },
|
|
386
|
-
description: "Multiple block indices (0-based flattened).",
|
|
387
|
-
},
|
|
388
|
-
startIndex: {
|
|
389
|
-
type: "number",
|
|
390
|
-
description: "Range selection start index.",
|
|
391
|
-
},
|
|
392
|
-
endIndex: {
|
|
393
|
-
type: "number",
|
|
394
|
-
description: "Range selection end index.",
|
|
395
|
-
},
|
|
396
|
-
section: {
|
|
397
|
-
type: "string",
|
|
398
|
-
description: "Target section by heading text (partial match). Combines with blockType.",
|
|
399
|
-
},
|
|
400
|
-
blockType: {
|
|
401
|
-
type: "string",
|
|
402
|
-
description: "Filter by block type (e.g. core/table). Combines with section.",
|
|
403
|
-
},
|
|
404
|
-
typeIndex: {
|
|
405
|
-
type: "number",
|
|
406
|
-
description: "N-th block of the specified blockType (0-based). Use with blockType.",
|
|
407
|
-
},
|
|
408
|
-
headingLevel: {
|
|
409
|
-
type: "number",
|
|
410
|
-
description: "Heading level for section selection (2, 3, 4). Use with headingContains.",
|
|
411
|
-
},
|
|
412
|
-
headingContains: {
|
|
413
|
-
type: "string",
|
|
414
|
-
description: "Heading text for section selection. Use with headingLevel.",
|
|
415
|
-
},
|
|
836
|
+
target: targetSchema,
|
|
416
837
|
},
|
|
838
|
+
required: ["target"],
|
|
417
839
|
},
|
|
418
840
|
},
|
|
419
841
|
{
|
|
@@ -424,52 +846,7 @@ const tools = [
|
|
|
424
846
|
properties: {
|
|
425
847
|
postId: postIdParam,
|
|
426
848
|
site: siteParam,
|
|
427
|
-
target:
|
|
428
|
-
type: "string",
|
|
429
|
-
enum: ["selected"],
|
|
430
|
-
description: "Use 'selected' to target the block currently selected by the user in WordPress editor.",
|
|
431
|
-
},
|
|
432
|
-
index: {
|
|
433
|
-
type: "number",
|
|
434
|
-
description: "Single block index (0-based flattened).",
|
|
435
|
-
},
|
|
436
|
-
indices: {
|
|
437
|
-
type: "array",
|
|
438
|
-
items: { type: "number" },
|
|
439
|
-
description: "Multiple block indices (0-based flattened).",
|
|
440
|
-
},
|
|
441
|
-
startIndex: {
|
|
442
|
-
type: "number",
|
|
443
|
-
description: "Range selection start index.",
|
|
444
|
-
},
|
|
445
|
-
endIndex: {
|
|
446
|
-
type: "number",
|
|
447
|
-
description: "Range selection end index.",
|
|
448
|
-
},
|
|
449
|
-
section: {
|
|
450
|
-
type: "string",
|
|
451
|
-
description: "Target section by heading text (partial match). Combines with blockType/contains.",
|
|
452
|
-
},
|
|
453
|
-
blockType: {
|
|
454
|
-
type: "string",
|
|
455
|
-
description: "Filter by block type (e.g. core/table). Combines with section/contains.",
|
|
456
|
-
},
|
|
457
|
-
typeIndex: {
|
|
458
|
-
type: "number",
|
|
459
|
-
description: "N-th block of the specified blockType (0-based). Use with blockType.",
|
|
460
|
-
},
|
|
461
|
-
contains: {
|
|
462
|
-
type: "string",
|
|
463
|
-
description: "Target blocks containing this text.",
|
|
464
|
-
},
|
|
465
|
-
headingLevel: {
|
|
466
|
-
type: "number",
|
|
467
|
-
description: "Heading level for section selection (2, 3, 4). Use with headingContains.",
|
|
468
|
-
},
|
|
469
|
-
headingContains: {
|
|
470
|
-
type: "string",
|
|
471
|
-
description: "Heading text for section selection. Use with headingLevel.",
|
|
472
|
-
},
|
|
849
|
+
target: targetSchema,
|
|
473
850
|
replacements: {
|
|
474
851
|
type: "array",
|
|
475
852
|
items: {
|
|
@@ -485,7 +862,11 @@ const tools = [
|
|
|
485
862
|
},
|
|
486
863
|
newHTML: {
|
|
487
864
|
type: "string",
|
|
488
|
-
description: "Full HTML replacement (Gutenberg markup). Only for contiguous targets
|
|
865
|
+
description: "Full HTML replacement (Gutenberg markup). Only for contiguous targets. Mutually exclusive with filePath.",
|
|
866
|
+
},
|
|
867
|
+
filePath: {
|
|
868
|
+
type: "string",
|
|
869
|
+
description: "Absolute path to a local file containing Gutenberg HTML. Read as UTF-8. Mutually exclusive with newHTML. Cannot be used with replacements or attributeUpdates.",
|
|
489
870
|
},
|
|
490
871
|
attributeUpdates: {
|
|
491
872
|
type: "object",
|
|
@@ -495,24 +876,18 @@ const tools = [
|
|
|
495
876
|
},
|
|
496
877
|
description: "Direct attribute updates via updateBlockAttributes API.",
|
|
497
878
|
},
|
|
498
|
-
|
|
499
|
-
type: "boolean",
|
|
500
|
-
description: "Keep existing blocks, insert newHTML as new blocks. Only with newHTML.",
|
|
501
|
-
},
|
|
502
|
-
insertPosition: {
|
|
503
|
-
type: ["number", "string"],
|
|
504
|
-
description: "Insert position when insertOnly=true. 'before', 'after' (default), or number.",
|
|
505
|
-
},
|
|
879
|
+
insert: insertSchema,
|
|
506
880
|
dryRun: {
|
|
507
881
|
type: "boolean",
|
|
508
882
|
description: "Preview changes without applying.",
|
|
509
883
|
},
|
|
510
884
|
},
|
|
885
|
+
required: ["target"],
|
|
511
886
|
},
|
|
512
887
|
},
|
|
513
888
|
{
|
|
514
889
|
name: "table_operations",
|
|
515
|
-
description: "Perform structured operations on core/table blocks (get structure, update cells, add/delete rows/columns
|
|
890
|
+
description: "Perform structured operations on core/table blocks (get structure, update/move cells/rows/columns, add/delete rows/columns).",
|
|
516
891
|
inputSchema: {
|
|
517
892
|
type: "object",
|
|
518
893
|
properties: {
|
|
@@ -524,7 +899,7 @@ const tools = [
|
|
|
524
899
|
},
|
|
525
900
|
action: {
|
|
526
901
|
type: "string",
|
|
527
|
-
enum: ["get_structure", "update_cell", "add_row", "delete_row", "add_column", "delete_column", "
|
|
902
|
+
enum: ["get_structure", "update_cell", "add_row", "delete_row", "add_column", "delete_column", "move_row", "move_column", "update_row", "update_column"],
|
|
528
903
|
description: "Operation type.",
|
|
529
904
|
},
|
|
530
905
|
row: {
|
|
@@ -541,28 +916,33 @@ const tools = [
|
|
|
541
916
|
},
|
|
542
917
|
position: {
|
|
543
918
|
type: "number",
|
|
544
|
-
description: "
|
|
919
|
+
description: "Position index. add_row/add_column: insert-before index (omit for end). move_row/move_column: destination index after move (required).",
|
|
545
920
|
},
|
|
546
921
|
cells: {
|
|
547
922
|
type: "array",
|
|
548
923
|
items: { type: "string" },
|
|
549
|
-
description: "Cell contents
|
|
550
|
-
},
|
|
551
|
-
style: {
|
|
552
|
-
type: "object",
|
|
553
|
-
properties: {
|
|
554
|
-
backgroundColor: { type: "string", description: "Background color (e.g. #ff0000)" },
|
|
555
|
-
color: { type: "string", description: "Text color" },
|
|
556
|
-
fontWeight: { type: "string", description: "Font weight (bold, normal)" },
|
|
557
|
-
textAlign: { type: "string", description: "Text alignment (left, center, right)" },
|
|
558
|
-
fontSize: { type: "string", description: "Font size (e.g. 14px)" },
|
|
559
|
-
},
|
|
560
|
-
description: "Style for style_cell/style_row.",
|
|
924
|
+
description: "Cell contents array. add_row: new row cells. add_column: initial column values. update_row/update_column: replacement values. Omit for empty cells.",
|
|
561
925
|
},
|
|
562
926
|
},
|
|
563
927
|
required: ["index", "action"],
|
|
564
928
|
},
|
|
565
929
|
},
|
|
930
|
+
{
|
|
931
|
+
name: "open_in_browser",
|
|
932
|
+
description: "Open post in the OS default browser. Opens editor (wp-admin) or front page. Uses the user's default browser profile (logged-in session).",
|
|
933
|
+
inputSchema: {
|
|
934
|
+
type: "object",
|
|
935
|
+
properties: {
|
|
936
|
+
postId: postIdParam,
|
|
937
|
+
site: siteParam,
|
|
938
|
+
target: {
|
|
939
|
+
type: "string",
|
|
940
|
+
enum: ["editor", "front"],
|
|
941
|
+
description: "Open target. 'editor' = wp-admin edit screen, 'front' = public permalink page. Default: 'editor'.",
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
566
946
|
{
|
|
567
947
|
name: "list_connections",
|
|
568
948
|
description: "List available site/user connections.",
|
|
@@ -601,20 +981,327 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
601
981
|
return { tools };
|
|
602
982
|
});
|
|
603
983
|
|
|
984
|
+
/**
|
|
985
|
+
* update_blocks ツールの共通ハンドラ(insert_block からの委譲にも対応)
|
|
986
|
+
* @param {object} args - ツール引数
|
|
987
|
+
* @param {string} toolName - 呼び出し元ツール名(レスポンス表示用)
|
|
988
|
+
*/
|
|
989
|
+
async function handleUpdateBlocksTool(args, toolName) {
|
|
990
|
+
const name = toolName || "update_blocks";
|
|
991
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
992
|
+
if (mode === 'error') {
|
|
993
|
+
return errorResponse(name, message, args?.site);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// slug → postId 解決(headless モード時のみ)
|
|
997
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
998
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
999
|
+
const postId = _resolved.postId ?? _postId;
|
|
1000
|
+
|
|
1001
|
+
// --- 旧フラットパラメータの混在を検出(後方互換なし方針) ---
|
|
1002
|
+
const legacyKeys = ['insertOnly', 'insertPosition', 'startIndex', 'endIndex',
|
|
1003
|
+
'headingLevel', 'headingContains', 'typeIndex'];
|
|
1004
|
+
const foundLegacy = legacyKeys.filter(k => args?.[k] !== undefined);
|
|
1005
|
+
if (foundLegacy.length > 0) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [{ type: "text", text: `❌ 旧パラメータ形式は廃止されました: ${foundLegacy.join(', ')}。target / insert オブジェクトを使用してください。` }],
|
|
1008
|
+
isError: true,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// --- normalizeTarget / normalizeInsert ---
|
|
1013
|
+
let tp, ins;
|
|
1014
|
+
try {
|
|
1015
|
+
tp = normalizeTarget(args?.target);
|
|
1016
|
+
ins = normalizeInsert(args?.insert);
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex,
|
|
1022
|
+
contains, headingLevel, headingContains } = tp;
|
|
1023
|
+
const { insertOnly, insertPosition } = ins;
|
|
1024
|
+
const { replacements, attributeUpdates, dryRun,
|
|
1025
|
+
_fromInsertBlock, appendToEnd } = (args || {});
|
|
1026
|
+
let { newHTML } = (args || {});
|
|
1027
|
+
|
|
1028
|
+
// --- filePath → newHTML 解決(update_blocks 直接呼び出し時) ---
|
|
1029
|
+
if (args?.filePath) {
|
|
1030
|
+
if (newHTML) {
|
|
1031
|
+
return { content: [{ type: "text", text: "❌ newHTML と filePath は同時に指定できません。どちらか一方を指定してください。" }], isError: true };
|
|
1032
|
+
}
|
|
1033
|
+
if (replacements && replacements.length > 0) {
|
|
1034
|
+
return { content: [{ type: "text", text: "❌ filePath は replacements と併用できません。" }], isError: true };
|
|
1035
|
+
}
|
|
1036
|
+
if (attributeUpdates) {
|
|
1037
|
+
return { content: [{ type: "text", text: "❌ filePath は attributeUpdates と併用できません。" }], isError: true };
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
newHTML = readHTMLFromFile(args.filePath).html;
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// --- insert 排他チェック ---
|
|
1047
|
+
if (insertOnly && (replacements?.length > 0 || attributeUpdates)) {
|
|
1048
|
+
return {
|
|
1049
|
+
content: [{ type: "text", text: "❌ insert は newHTML 専用です。replacements や attributeUpdates と併用できません。" }],
|
|
1050
|
+
isError: true,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
if (insertOnly && !newHTML) {
|
|
1054
|
+
return {
|
|
1055
|
+
content: [{ type: "text", text: "❌ insert には newHTML が必要です。" }],
|
|
1056
|
+
isError: true,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Headless モードで target: "selected" はエラー
|
|
1061
|
+
if (mode === 'headless' && target === 'selected') {
|
|
1062
|
+
return {
|
|
1063
|
+
content: [{ type: "text", text: "❌ target:'selected' はエディタ接続時のみ使用可能です。index を指定してください。" }],
|
|
1064
|
+
isError: true,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// --- ターゲットチェック(appendToEnd 時は不要) ---
|
|
1069
|
+
const hasTarget = target || index !== undefined || indices ||
|
|
1070
|
+
startIndex !== undefined || section || blockType || contains ||
|
|
1071
|
+
(headingLevel && headingContains);
|
|
1072
|
+
if (!hasTarget && !(appendToEnd && insertOnly)) {
|
|
1073
|
+
return {
|
|
1074
|
+
content: [{ type: "text", text: "❌ ターゲットが指定されていません。\ntarget, index, indices, section, blockType, contains 等のいずれかを指定してください。" }],
|
|
1075
|
+
isError: true,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const hasUpdate = (replacements && replacements.length > 0) || newHTML || attributeUpdates;
|
|
1080
|
+
if (!hasUpdate) {
|
|
1081
|
+
return {
|
|
1082
|
+
content: [{ type: "text", text: "❌ 変更内容が指定されていません。\nreplacements, newHTML, attributeUpdates のいずれかを指定してください。" }],
|
|
1083
|
+
isError: true,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// --- insertOnly 非推奨警告(外部呼び出し時のみ) ---
|
|
1088
|
+
let deprecationWarning = '';
|
|
1089
|
+
if (insertOnly && !_fromInsertBlock) {
|
|
1090
|
+
deprecationWarning = '\n⚠️ update_blocks の insertOnly は非推奨です。insert_block ツールを使用してください。';
|
|
1091
|
+
console.error('[DEPRECATED] update_blocks: insertOnly は非推奨です。insert_block を使用してください。');
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ========================================
|
|
1095
|
+
// Headless モード
|
|
1096
|
+
// ========================================
|
|
1097
|
+
if (mode === 'headless') {
|
|
1098
|
+
// appendToEnd: ターゲット不要の末尾追加
|
|
1099
|
+
if (appendToEnd && insertOnly && newHTML) {
|
|
1100
|
+
const result = await client.headlessUpdate(postId, {
|
|
1101
|
+
newHTML,
|
|
1102
|
+
insertOnly: true,
|
|
1103
|
+
appendToEnd: true,
|
|
1104
|
+
_fromInsertBlock: _fromInsertBlock || false,
|
|
1105
|
+
});
|
|
1106
|
+
const count = result.results?.[0]?.count || 1;
|
|
1107
|
+
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at end${deprecationWarning}` }] };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const result = await client.headlessUpdate(postId, {
|
|
1111
|
+
index, indices, startIndex, endIndex,
|
|
1112
|
+
section, blockType, typeIndex, contains,
|
|
1113
|
+
headingLevel, headingContains,
|
|
1114
|
+
replacements, newHTML, attributeUpdates,
|
|
1115
|
+
insertOnly: insertOnly || false,
|
|
1116
|
+
insertPosition,
|
|
1117
|
+
dryRun: dryRun || false,
|
|
1118
|
+
_fromInsertBlock: _fromInsertBlock || false,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
if (result.dryRun && result.results) {
|
|
1122
|
+
const previewText = result.results
|
|
1123
|
+
.map(r => {
|
|
1124
|
+
const label = r.index !== undefined ? `[${r.index}]` :
|
|
1125
|
+
r.indices ? `[${r.indices.join(', ')}]` : '[?]';
|
|
1126
|
+
return ` ${label}: ${r.preview ? 'プレビューあり' : 'N/A'}`;
|
|
1127
|
+
})
|
|
1128
|
+
.join('\n');
|
|
1129
|
+
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}${deprecationWarning}` }] };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (result.results) {
|
|
1133
|
+
const successCount = result.results.filter(r => r.success).length;
|
|
1134
|
+
const failCount = result.results.filter(r => !r.success).length;
|
|
1135
|
+
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}` }] };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return { content: [{ type: "text", text: `✅ 更新完了${deprecationWarning}` }] };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ========================================
|
|
1142
|
+
// Editor モード
|
|
1143
|
+
// ========================================
|
|
1144
|
+
|
|
1145
|
+
// appendToEnd: ターゲット不要の末尾追加
|
|
1146
|
+
if (appendToEnd && insertOnly && newHTML) {
|
|
1147
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1148
|
+
newHTML,
|
|
1149
|
+
insertOnly: true,
|
|
1150
|
+
appendToEnd: true,
|
|
1151
|
+
}, 10000);
|
|
1152
|
+
if (!result) return timeoutResponse(name, client, args?.site);
|
|
1153
|
+
if (!result.success) return errorResponse(name, result.error, args?.site);
|
|
1154
|
+
const count = result.inserted || 1;
|
|
1155
|
+
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at end${deprecationWarning}` }] };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
1159
|
+
if (target === "selected") {
|
|
1160
|
+
const state = await client.getEditorState();
|
|
1161
|
+
const sel = state.selectedBlock;
|
|
1162
|
+
|
|
1163
|
+
if (!sel || (!sel.blockId && !sel.blockIds)) {
|
|
1164
|
+
return {
|
|
1165
|
+
content: [{ type: "text", text: "❌ ブロックが選択されていません。" }],
|
|
1166
|
+
isError: true,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const blockIndices = sel.isMultiSelect ? sel.blockIndices : [sel.blockIndex];
|
|
1171
|
+
|
|
1172
|
+
// replacementsの場合、MCP側でカーソル選択ピンポイント差分を適用
|
|
1173
|
+
if (replacements && replacements.length > 0) {
|
|
1174
|
+
if (!sel.blockHTML) {
|
|
1175
|
+
return {
|
|
1176
|
+
content: [{ type: "text", text: "❌ 選択中ブロックのHTML情報がありません。" }],
|
|
1177
|
+
isError: true,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
let finalHTML = sel.blockHTML;
|
|
1181
|
+
const hasTextSelection = sel.textSelection && sel.textSelection.text;
|
|
1182
|
+
for (const replacement of replacements) {
|
|
1183
|
+
if (!replacement.old || !replacement.new) continue;
|
|
1184
|
+
if (hasTextSelection && sel.textSelection) {
|
|
1185
|
+
const { text: selText, context } = sel.textSelection;
|
|
1186
|
+
const searchPattern = context.before + selText + context.after;
|
|
1187
|
+
if (finalHTML.includes(searchPattern)) {
|
|
1188
|
+
const replacedPattern = context.before + replacement.new + context.after;
|
|
1189
|
+
finalHTML = finalHTML.replace(searchPattern, replacedPattern);
|
|
1190
|
+
} else {
|
|
1191
|
+
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1198
|
+
mode: "html_replace_by_index",
|
|
1199
|
+
blockIndices,
|
|
1200
|
+
newHTML: finalHTML,
|
|
1201
|
+
insertOnly: insertOnly || false,
|
|
1202
|
+
insertPosition,
|
|
1203
|
+
}, 10000);
|
|
1204
|
+
if (!result)
|
|
1205
|
+
return timeoutResponse(name, client, args?.site);
|
|
1206
|
+
if (!result.success)
|
|
1207
|
+
return errorResponse(name, result.error, args?.site);
|
|
1208
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}` }] };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// newHTMLの場合
|
|
1212
|
+
if (newHTML) {
|
|
1213
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1214
|
+
mode: "html_replace_by_index",
|
|
1215
|
+
blockIndices,
|
|
1216
|
+
newHTML,
|
|
1217
|
+
insertOnly: insertOnly || false,
|
|
1218
|
+
insertPosition,
|
|
1219
|
+
}, 10000);
|
|
1220
|
+
if (!result)
|
|
1221
|
+
return timeoutResponse(name, client, args?.site);
|
|
1222
|
+
if (!result.success)
|
|
1223
|
+
return errorResponse(name, result.error, args?.site);
|
|
1224
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}` }] };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// attributeUpdatesの場合
|
|
1228
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1229
|
+
mode: "attribute_update_by_index",
|
|
1230
|
+
blockIndices,
|
|
1231
|
+
attributeUpdates,
|
|
1232
|
+
dryRun: dryRun || false,
|
|
1233
|
+
}, 10000);
|
|
1234
|
+
if (!result)
|
|
1235
|
+
return timeoutResponse(name, client, args?.site);
|
|
1236
|
+
if (!result.success)
|
|
1237
|
+
return errorResponse(name, result.error, args?.site);
|
|
1238
|
+
return { content: [{ type: "text", text: `✅ 属性更新完了${deprecationWarning}` }] };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// その他のターゲット → 全てWP側で解決&実行
|
|
1242
|
+
const maxWait = (section || blockType || contains) ? 15000 : 10000;
|
|
1243
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1244
|
+
index, indices, startIndex, endIndex,
|
|
1245
|
+
section, blockType, typeIndex, contains,
|
|
1246
|
+
headingLevel, headingContains,
|
|
1247
|
+
replacements, newHTML, attributeUpdates,
|
|
1248
|
+
insertOnly: insertOnly || false,
|
|
1249
|
+
insertPosition,
|
|
1250
|
+
dryRun: dryRun || false,
|
|
1251
|
+
_fromInsertBlock: _fromInsertBlock || false,
|
|
1252
|
+
}, maxWait);
|
|
1253
|
+
if (!result)
|
|
1254
|
+
return timeoutResponse(name, client, args?.site);
|
|
1255
|
+
if (!result.success)
|
|
1256
|
+
return errorResponse(name, result.error, args?.site);
|
|
1257
|
+
|
|
1258
|
+
// 結果フォーマット
|
|
1259
|
+
if (dryRun && result.results) {
|
|
1260
|
+
const previewText = result.results
|
|
1261
|
+
.filter(r => r.preview)
|
|
1262
|
+
.map(r => {
|
|
1263
|
+
if (Array.isArray(r.preview)) {
|
|
1264
|
+
return ` [${r.index}]\n${r.preview.map(p => ` "${p.old}" → "${p.new}" (${p.count}箇所)`).join('\n')}`;
|
|
1265
|
+
} else if (r.preview?.willUpdate) {
|
|
1266
|
+
return ` [${r.index}]: 属性更新予定 ${JSON.stringify(r.preview.willUpdate)}`;
|
|
1267
|
+
} else if (r.preview?.skipped) {
|
|
1268
|
+
return ` [${r.index}]: スキップ (${r.preview.skipped})`;
|
|
1269
|
+
}
|
|
1270
|
+
return ` [${r.index}]: プレビュー不可`;
|
|
1271
|
+
})
|
|
1272
|
+
.join('\n');
|
|
1273
|
+
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}${deprecationWarning}` }] };
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (result.results) {
|
|
1277
|
+
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
1278
|
+
const skipCount = result.results.filter(r => r.skipped).length;
|
|
1279
|
+
const failCount = result.results.filter(r => !r.success).length;
|
|
1280
|
+
const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
|
|
1281
|
+
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}` }] };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}` }] };
|
|
1285
|
+
}
|
|
1286
|
+
|
|
604
1287
|
// ツール実行のハンドラ
|
|
605
1288
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
606
1289
|
const { name, arguments: args } = request.params;
|
|
607
1290
|
try {
|
|
608
1291
|
switch (name) {
|
|
609
1292
|
case "get_article_structure": {
|
|
610
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1293
|
+
const { mode, postId: _postId, message, client, siteName } = await resolveMode(args);
|
|
611
1294
|
if (mode === 'error') {
|
|
612
1295
|
return errorResponse(name, message, args?.site);
|
|
613
1296
|
}
|
|
1297
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1298
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1299
|
+
const postId = _resolved.postId ?? _postId;
|
|
614
1300
|
|
|
615
1301
|
const { blockType, section, headingLevel, expand, contains, full, limit, offset } = (args || {});
|
|
616
1302
|
|
|
617
1303
|
// 使用パラメータを特定
|
|
1304
|
+
const siteLabel = siteName || 'default';
|
|
618
1305
|
const usedParams = [];
|
|
619
1306
|
if (section) usedParams.push("section");
|
|
620
1307
|
if (blockType) usedParams.push("blockType");
|
|
@@ -624,7 +1311,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
624
1311
|
if (full) usedParams.push("full");
|
|
625
1312
|
if (limit && limit > 0) usedParams.push(`limit=${limit}`);
|
|
626
1313
|
if (offset && offset > 0) usedParams.push(`offset=${offset}`);
|
|
627
|
-
const paramLabel = usedParams.length > 0
|
|
1314
|
+
const paramLabel = usedParams.length > 0
|
|
1315
|
+
? `${siteLabel}, ${usedParams.join(" + ")}`
|
|
1316
|
+
: siteLabel;
|
|
628
1317
|
|
|
629
1318
|
// Headless / Editor でデータ取得を分岐
|
|
630
1319
|
let state;
|
|
@@ -1035,13 +1724,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1035
1724
|
return { content: [{ type: "text", text: `❌ select_block はエディタ接続時のみ使用可能です。` }], isError: true };
|
|
1036
1725
|
}
|
|
1037
1726
|
|
|
1038
|
-
const { index, blockType, typeIndex, contains, startIndex, endIndex, headingLevel, headingContains } = (args || {});
|
|
1039
|
-
|
|
1040
1727
|
try {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1728
|
+
const tp = normalizeTarget(args?.target);
|
|
1729
|
+
|
|
1730
|
+
// BUG-3: select_block では selected は使用不可
|
|
1731
|
+
if (tp.target === "selected") {
|
|
1732
|
+
return {
|
|
1733
|
+
content: [{ type: "text", text: "❌ select_block では selected は使用できません。index, blockType, range 等を指定してください。" }],
|
|
1734
|
+
isError: true,
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// BUG-2: hasTarget チェック
|
|
1739
|
+
const { target, index, indices, startIndex, endIndex, section, blockType,
|
|
1740
|
+
typeIndex, contains, headingLevel, headingContains } = tp;
|
|
1741
|
+
const hasTarget = target || index !== undefined || indices ||
|
|
1742
|
+
startIndex !== undefined || section || blockType || contains ||
|
|
1743
|
+
(headingLevel && headingContains);
|
|
1744
|
+
if (!hasTarget) {
|
|
1745
|
+
return {
|
|
1746
|
+
content: [{ type: "text", text: "❌ ターゲットが指定されていません。index, blockType, range, heading, contains 等のいずれかを指定してください。" }],
|
|
1747
|
+
isError: true,
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
await client.selectBlock(tp);
|
|
1045
1752
|
|
|
1046
1753
|
return {
|
|
1047
1754
|
content: [{
|
|
@@ -1061,10 +1768,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1061
1768
|
}
|
|
1062
1769
|
|
|
1063
1770
|
case "delete_block": {
|
|
1064
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1771
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1065
1772
|
if (mode === 'error') {
|
|
1066
1773
|
return errorResponse(name, message, args?.site);
|
|
1067
1774
|
}
|
|
1775
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1776
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1777
|
+
const postId = _resolved.postId ?? _postId;
|
|
1068
1778
|
|
|
1069
1779
|
const { index, count, indices } = args;
|
|
1070
1780
|
|
|
@@ -1119,12 +1829,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1119
1829
|
}
|
|
1120
1830
|
|
|
1121
1831
|
case "move_block": {
|
|
1122
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1832
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1123
1833
|
if (mode === 'error') {
|
|
1124
1834
|
return errorResponse(name, message, args?.site);
|
|
1125
1835
|
}
|
|
1836
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1837
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1838
|
+
const postId = _resolved.postId ?? _postId;
|
|
1126
1839
|
|
|
1127
1840
|
const { from, to, fromFlat, toFlat } = args;
|
|
1841
|
+
const count = args.count ?? 1;
|
|
1842
|
+
|
|
1843
|
+
// count バリデーション
|
|
1844
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
1845
|
+
return { content: [{ type: "text", text: "❌ count は1以上の整数を指定してください。" }], isError: true };
|
|
1846
|
+
}
|
|
1128
1847
|
|
|
1129
1848
|
// 排他バリデーション
|
|
1130
1849
|
const hasTopLevel = from !== undefined || to !== undefined;
|
|
@@ -1136,26 +1855,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1136
1855
|
return { content: [{ type: "text", text: "❌ from/to または fromFlat/toFlat を指定してください。" }], isError: true };
|
|
1137
1856
|
}
|
|
1138
1857
|
|
|
1858
|
+
// 移動結果のレスポンス生成ヘルパー
|
|
1859
|
+
const moveMsg = (moved) => {
|
|
1860
|
+
if (moved?.noop) return `✅ 移動不要(同じ位置)`;
|
|
1861
|
+
const typeLabel = moved?.types ? moved.types.join(', ') : moved?.type ?? 'unknown';
|
|
1862
|
+
const countLabel = (moved?.count ?? 1) > 1 ? `${moved.count}ブロック` : '';
|
|
1863
|
+
return countLabel
|
|
1864
|
+
? `✅ ${countLabel}移動 (${typeLabel})`
|
|
1865
|
+
: `✅ ブロック移動 (${typeLabel})`;
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1139
1868
|
// fromFlat/toFlat モード(フラットインデックス移動)
|
|
1140
1869
|
if (hasFlat) {
|
|
1141
1870
|
if (fromFlat === undefined || toFlat === undefined) {
|
|
1142
1871
|
return { content: [{ type: "text", text: "❌ fromFlat と toFlat の両方を指定してください。" }], isError: true };
|
|
1143
1872
|
}
|
|
1144
|
-
if (fromFlat === toFlat) {
|
|
1145
|
-
return { content: [{ type: "text", text:
|
|
1873
|
+
if (fromFlat === toFlat || toFlat === fromFlat + count) {
|
|
1874
|
+
return { content: [{ type: "text", text: `✅ 移動不要(同じ位置)` }] };
|
|
1146
1875
|
}
|
|
1147
1876
|
|
|
1148
1877
|
if (mode === 'headless') {
|
|
1149
|
-
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat);
|
|
1150
|
-
return { content: [{ type: "text", text:
|
|
1878
|
+
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
|
|
1879
|
+
return { content: [{ type: "text", text: moveMsg(result.moved) }] };
|
|
1151
1880
|
}
|
|
1152
1881
|
|
|
1153
|
-
const result = await client.sendEditorCommand("move_block", { fromFlat, toFlat });
|
|
1882
|
+
const result = await client.sendEditorCommand("move_block", { fromFlat, toFlat, count });
|
|
1154
1883
|
if (!result)
|
|
1155
1884
|
return timeoutResponse(name, client, args?.site);
|
|
1156
1885
|
if (!result.success)
|
|
1157
1886
|
return errorResponse(name, result.error, args?.site);
|
|
1158
|
-
return { content: [{ type: "text", text:
|
|
1887
|
+
return { content: [{ type: "text", text: moveMsg(result.moved) }] };
|
|
1159
1888
|
}
|
|
1160
1889
|
|
|
1161
1890
|
// 既存モード(from/to トップレベル)
|
|
@@ -1164,18 +1893,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1164
1893
|
}
|
|
1165
1894
|
|
|
1166
1895
|
if (mode === 'headless') {
|
|
1167
|
-
const result = await client.headlessMove(postId, from, to);
|
|
1168
|
-
return { content: [{ type: "text", text:
|
|
1896
|
+
const result = await client.headlessMove(postId, from, to, count);
|
|
1897
|
+
return { content: [{ type: "text", text: moveMsg(result.moved) }] };
|
|
1169
1898
|
}
|
|
1170
1899
|
|
|
1171
|
-
const result = await client.sendEditorCommand("move_block", { from, to });
|
|
1900
|
+
const result = await client.sendEditorCommand("move_block", { from, to, count });
|
|
1172
1901
|
if (!result)
|
|
1173
1902
|
return timeoutResponse(name, client, args?.site);
|
|
1174
1903
|
if (!result.success)
|
|
1175
1904
|
return errorResponse(name, result.error, args?.site);
|
|
1176
|
-
|
|
1177
|
-
return { content: [{ type: "text", text: `✅ 移動不要(同じ位置)` }] };
|
|
1178
|
-
return { content: [{ type: "text", text: `✅ ブロック移動: [${from}] → [${to}] (${result.moved.type})` }] };
|
|
1905
|
+
return { content: [{ type: "text", text: moveMsg(result.moved) }] };
|
|
1179
1906
|
}
|
|
1180
1907
|
|
|
1181
1908
|
case "undo": {
|
|
@@ -1211,10 +1938,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1211
1938
|
}
|
|
1212
1939
|
|
|
1213
1940
|
case "duplicate_block": {
|
|
1214
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1941
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1215
1942
|
if (mode === 'error') {
|
|
1216
1943
|
return errorResponse(name, message, args?.site);
|
|
1217
1944
|
}
|
|
1945
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1946
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1947
|
+
const postId = _resolved.postId ?? _postId;
|
|
1218
1948
|
|
|
1219
1949
|
const { index } = args;
|
|
1220
1950
|
|
|
@@ -1235,10 +1965,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1235
1965
|
}
|
|
1236
1966
|
|
|
1237
1967
|
case "save_post": {
|
|
1238
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1968
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1239
1969
|
if (mode === 'error') {
|
|
1240
1970
|
return errorResponse(name, message, args?.site);
|
|
1241
1971
|
}
|
|
1972
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1973
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1974
|
+
const postId = _resolved.postId ?? _postId;
|
|
1242
1975
|
|
|
1243
1976
|
if (mode === 'headless') {
|
|
1244
1977
|
// Headless モードでは即 DB 書き込みのため save は不要
|
|
@@ -1254,10 +1987,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1254
1987
|
}
|
|
1255
1988
|
|
|
1256
1989
|
case "get_post_meta": {
|
|
1257
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
1990
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1258
1991
|
if (mode === 'error') {
|
|
1259
1992
|
return errorResponse(name, message, args?.site);
|
|
1260
1993
|
}
|
|
1994
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
1995
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
1996
|
+
const postId = _resolved.postId ?? _postId;
|
|
1261
1997
|
|
|
1262
1998
|
let m;
|
|
1263
1999
|
if (mode === 'headless') {
|
|
@@ -1293,10 +2029,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1293
2029
|
}
|
|
1294
2030
|
|
|
1295
2031
|
case "update_post_meta": {
|
|
1296
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
2032
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1297
2033
|
if (mode === 'error') {
|
|
1298
2034
|
return errorResponse(name, message, args?.site);
|
|
1299
2035
|
}
|
|
2036
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
2037
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
2038
|
+
const postId = _resolved.postId ?? _postId;
|
|
1300
2039
|
|
|
1301
2040
|
const updateData = {};
|
|
1302
2041
|
if (args.title !== undefined) updateData.title = args.title;
|
|
@@ -1330,7 +2069,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1330
2069
|
} catch (e) {
|
|
1331
2070
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1332
2071
|
}
|
|
1333
|
-
|
|
2072
|
+
|
|
2073
|
+
// slug/search の空白正規化
|
|
2074
|
+
const slug = args?.slug?.trim() || undefined;
|
|
2075
|
+
const search = args?.search?.trim() || undefined;
|
|
2076
|
+
|
|
2077
|
+
// slug と search の排他チェック
|
|
2078
|
+
if (slug && search) {
|
|
2079
|
+
return { content: [{ type: "text", text: "❌ slug と search は同時に指定できません。slug(完全一致)または search(キーワード検索)のいずれかを使用してください。" }], isError: true };
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// listArgs を構築(slug 指定時: status 未指定なら 'any' に自動設定)
|
|
2083
|
+
const listArgs = { ...(args || {}), slug, search };
|
|
2084
|
+
if (slug && !listArgs.status) {
|
|
2085
|
+
listArgs.status = 'any';
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const { data: posts, total, totalPages } = await client.listPosts(listArgs);
|
|
1334
2089
|
|
|
1335
2090
|
if (!posts || posts.length === 0) {
|
|
1336
2091
|
return { content: [{ type: "text", text: "📋 該当する投稿はありません。" }] };
|
|
@@ -1393,77 +2148,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1393
2148
|
}
|
|
1394
2149
|
|
|
1395
2150
|
case "insert_block": {
|
|
1396
|
-
|
|
1397
|
-
if (mode === 'error') {
|
|
1398
|
-
return errorResponse(name, message, args?.site);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
const { blockType, content, index, blocks, rawHTML } = args;
|
|
2151
|
+
let { rawHTML, filePath, index } = (args || {});
|
|
1402
2152
|
|
|
1403
|
-
//
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
const hasRawHTML = rawHTML !== undefined;
|
|
1407
|
-
const modeCount = [hasLegacy, hasBlocks, hasRawHTML].filter(Boolean).length;
|
|
1408
|
-
if (modeCount === 0) {
|
|
1409
|
-
return { content: [{ type: "text", text: "❌ blockType+content、blocks、rawHTML のいずれかを指定してください。" }], isError: true };
|
|
2153
|
+
// 排他チェック
|
|
2154
|
+
if (rawHTML && filePath) {
|
|
2155
|
+
return { content: [{ type: "text", text: "❌ rawHTML と filePath は同時に指定できません。どちらか一方を指定してください。" }], isError: true };
|
|
1410
2156
|
}
|
|
1411
|
-
if (modeCount > 1) {
|
|
1412
|
-
return { content: [{ type: "text", text: "❌ blockType+content、blocks、rawHTML は同時に指定できません。" }], isError: true };
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
// 複数挿入モード (blocks / rawHTML)
|
|
1416
|
-
if (hasBlocks || hasRawHTML) {
|
|
1417
|
-
if (hasBlocks && (!Array.isArray(blocks) || blocks.length === 0)) {
|
|
1418
|
-
return { content: [{ type: "text", text: "❌ blocks は1つ以上の要素を含む配列で指定してください。" }], isError: true };
|
|
1419
|
-
}
|
|
1420
2157
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const result = await client.headlessInsert(postId, params);
|
|
1426
|
-
const count = result.insertedIndices ? result.insertedIndices.length : result.count || 1;
|
|
1427
|
-
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at ${index ?? 'end'}` }] };
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
const cmdParams = { index };
|
|
1431
|
-
if (hasBlocks) cmdParams.blocks = blocks;
|
|
1432
|
-
if (hasRawHTML) cmdParams.rawHTML = rawHTML;
|
|
1433
|
-
const result = await client.sendEditorCommand("insert_block", cmdParams);
|
|
1434
|
-
if (!result)
|
|
1435
|
-
return timeoutResponse(name, client, args?.site);
|
|
1436
|
-
if (!result.success)
|
|
1437
|
-
return errorResponse(name, result.error, args?.site);
|
|
1438
|
-
const count = result.insertedCount || result.inserted?.length || 1;
|
|
1439
|
-
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at ${index ?? 'end'}` }] };
|
|
2158
|
+
// filePath → rawHTML 解決
|
|
2159
|
+
if (filePath) {
|
|
2160
|
+
try { rawHTML = readHTMLFromFile(filePath).html; }
|
|
2161
|
+
catch (e) { return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true }; }
|
|
1440
2162
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (!blockType || content === undefined) {
|
|
1444
|
-
return { content: [{ type: "text", text: "❌ blockType と content は必須です" }], isError: true };
|
|
2163
|
+
if (!rawHTML) {
|
|
2164
|
+
return { content: [{ type: "text", text: "❌ rawHTML または filePath を指定してください。Gutenberg HTML 形式のブロックマークアップが必要です。" }], isError: true };
|
|
1445
2165
|
}
|
|
1446
2166
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return { content: [{ type: "text", text:
|
|
2167
|
+
// index バリデーション(現行 Editor 互換: 整数 >= 0)
|
|
2168
|
+
if (index !== undefined && (!Number.isInteger(index) || index < 0)) {
|
|
2169
|
+
return { content: [{ type: "text", text: `❌ Invalid index: ${index}` }], isError: true };
|
|
1450
2170
|
}
|
|
1451
2171
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2172
|
+
// update_blocks コードパスに委譲(新形式 target/insert を使用)
|
|
2173
|
+
const delegatedArgs = {
|
|
2174
|
+
postId: args.postId,
|
|
2175
|
+
site: args.site,
|
|
2176
|
+
newHTML: rawHTML,
|
|
2177
|
+
insert: { position: 'before' },
|
|
2178
|
+
_fromInsertBlock: true,
|
|
2179
|
+
appendToEnd: index === undefined,
|
|
2180
|
+
};
|
|
2181
|
+
if (index !== undefined) delegatedArgs.target = { index };
|
|
2182
|
+
return await handleUpdateBlocksTool(delegatedArgs, name);
|
|
1458
2183
|
}
|
|
1459
2184
|
|
|
1460
2185
|
case "get_block_html": {
|
|
1461
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
2186
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1462
2187
|
if (mode === 'error') {
|
|
1463
2188
|
return errorResponse(name, message, args?.site);
|
|
1464
2189
|
}
|
|
2190
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
2191
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
2192
|
+
const postId = _resolved.postId ?? _postId;
|
|
1465
2193
|
|
|
1466
|
-
|
|
2194
|
+
let tp;
|
|
2195
|
+
try { tp = normalizeTarget(args?.target); }
|
|
2196
|
+
catch (e) { return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true }; }
|
|
2197
|
+
|
|
2198
|
+
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex, contains, headingLevel, headingContains } = tp;
|
|
1467
2199
|
|
|
1468
2200
|
if (mode === 'headless' && target === 'selected') {
|
|
1469
2201
|
return {
|
|
@@ -1473,7 +2205,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1473
2205
|
}
|
|
1474
2206
|
|
|
1475
2207
|
const hasTarget = target || index !== undefined || indices ||
|
|
1476
|
-
startIndex !== undefined || section || blockType ||
|
|
2208
|
+
startIndex !== undefined || section || blockType || contains ||
|
|
1477
2209
|
(headingLevel && headingContains);
|
|
1478
2210
|
if (!hasTarget) {
|
|
1479
2211
|
return {
|
|
@@ -1486,12 +2218,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1486
2218
|
if (mode === 'headless') {
|
|
1487
2219
|
const result = await client.headlessGetBlockHtml(postId, {
|
|
1488
2220
|
index, indices, startIndex, endIndex,
|
|
1489
|
-
section, blockType, typeIndex,
|
|
2221
|
+
section, blockType, typeIndex, contains,
|
|
1490
2222
|
headingLevel, headingContains,
|
|
1491
2223
|
});
|
|
1492
2224
|
blocks = result.blocks || [];
|
|
1493
2225
|
} else {
|
|
1494
|
-
const result = await client.sendEditorCommand("get_block_html", {
|
|
2226
|
+
const result = await client.sendEditorCommand("get_block_html", {
|
|
2227
|
+
target, index, indices, startIndex, endIndex,
|
|
2228
|
+
section, blockType, typeIndex, contains,
|
|
2229
|
+
headingLevel, headingContains,
|
|
2230
|
+
}, 10000);
|
|
1495
2231
|
if (!result) {
|
|
1496
2232
|
return timeoutResponse(name, client, args?.site);
|
|
1497
2233
|
}
|
|
@@ -1545,210 +2281,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1545
2281
|
}
|
|
1546
2282
|
|
|
1547
2283
|
case "update_blocks": {
|
|
1548
|
-
|
|
1549
|
-
if (mode === 'error') {
|
|
1550
|
-
return errorResponse(name, message, args?.site);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex, contains, headingLevel, headingContains, replacements, newHTML, attributeUpdates, insertOnly, insertPosition, dryRun } = (args || {});
|
|
1554
|
-
|
|
1555
|
-
// Headless モードで target: "selected" はエラー
|
|
1556
|
-
if (mode === 'headless' && target === 'selected') {
|
|
1557
|
-
return {
|
|
1558
|
-
content: [{ type: "text", text: "❌ target:'selected' はエディタ接続時のみ使用可能です。index を指定してください。" }],
|
|
1559
|
-
isError: true,
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
const hasTarget = target || index !== undefined || indices ||
|
|
1564
|
-
startIndex !== undefined || section || blockType || contains ||
|
|
1565
|
-
(headingLevel && headingContains);
|
|
1566
|
-
if (!hasTarget) {
|
|
1567
|
-
return {
|
|
1568
|
-
content: [{ type: "text", text: "❌ ターゲットが指定されていません。\ntarget, index, indices, section, blockType, contains 等のいずれかを指定してください。" }],
|
|
1569
|
-
isError: true,
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
const hasUpdate = (replacements && replacements.length > 0) || newHTML || attributeUpdates;
|
|
1574
|
-
if (!hasUpdate) {
|
|
1575
|
-
return {
|
|
1576
|
-
content: [{ type: "text", text: "❌ 変更内容が指定されていません。\nreplacements, newHTML, attributeUpdates のいずれかを指定してください。" }],
|
|
1577
|
-
isError: true,
|
|
1578
|
-
};
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// ========================================
|
|
1582
|
-
// Headless モード
|
|
1583
|
-
// ========================================
|
|
1584
|
-
if (mode === 'headless') {
|
|
1585
|
-
const result = await client.headlessUpdate(modePostId, {
|
|
1586
|
-
index, indices, startIndex, endIndex,
|
|
1587
|
-
section, blockType, typeIndex, contains,
|
|
1588
|
-
headingLevel, headingContains,
|
|
1589
|
-
replacements, newHTML, attributeUpdates,
|
|
1590
|
-
insertOnly: insertOnly || false,
|
|
1591
|
-
insertPosition,
|
|
1592
|
-
dryRun: dryRun || false,
|
|
1593
|
-
});
|
|
1594
|
-
|
|
1595
|
-
if (result.dryRun && result.results) {
|
|
1596
|
-
const previewText = result.results
|
|
1597
|
-
.map(r => {
|
|
1598
|
-
const label = r.index !== undefined ? `[${r.index}]` :
|
|
1599
|
-
r.indices ? `[${r.indices.join(', ')}]` : '[?]';
|
|
1600
|
-
return ` ${label}: ${r.preview ? 'プレビューあり' : 'N/A'}`;
|
|
1601
|
-
})
|
|
1602
|
-
.join('\n');
|
|
1603
|
-
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}` }] };
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (result.results) {
|
|
1607
|
-
const successCount = result.results.filter(r => r.success).length;
|
|
1608
|
-
const failCount = result.results.filter(r => !r.success).length;
|
|
1609
|
-
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}` }] };
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
return { content: [{ type: "text", text: `✅ 更新完了` }] };
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// ========================================
|
|
1616
|
-
// Editor モード(現行ロジック)
|
|
1617
|
-
// ========================================
|
|
1618
|
-
|
|
1619
|
-
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
1620
|
-
if (target === "selected") {
|
|
1621
|
-
const state = await client.getEditorState();
|
|
1622
|
-
const sel = state.selectedBlock;
|
|
1623
|
-
|
|
1624
|
-
if (!sel || (!sel.blockId && !sel.blockIds)) {
|
|
1625
|
-
return {
|
|
1626
|
-
content: [{ type: "text", text: "❌ ブロックが選択されていません。" }],
|
|
1627
|
-
isError: true,
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
const blockIndices = sel.isMultiSelect ? sel.blockIndices : [sel.blockIndex];
|
|
1632
|
-
|
|
1633
|
-
// replacementsの場合、MCP側でカーソル選択ピンポイント差分を適用
|
|
1634
|
-
if (replacements && replacements.length > 0) {
|
|
1635
|
-
if (!sel.blockHTML) {
|
|
1636
|
-
return {
|
|
1637
|
-
content: [{ type: "text", text: "❌ 選択中ブロックのHTML情報がありません。" }],
|
|
1638
|
-
isError: true,
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
let finalHTML = sel.blockHTML;
|
|
1642
|
-
const hasTextSelection = sel.textSelection && sel.textSelection.text;
|
|
1643
|
-
for (const replacement of replacements) {
|
|
1644
|
-
if (!replacement.old || !replacement.new) continue;
|
|
1645
|
-
if (hasTextSelection && sel.textSelection) {
|
|
1646
|
-
const { text: selText, context } = sel.textSelection;
|
|
1647
|
-
const searchPattern = context.before + selText + context.after;
|
|
1648
|
-
if (finalHTML.includes(searchPattern)) {
|
|
1649
|
-
const replacedPattern = context.before + replacement.new + context.after;
|
|
1650
|
-
finalHTML = finalHTML.replace(searchPattern, replacedPattern);
|
|
1651
|
-
} else {
|
|
1652
|
-
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1653
|
-
}
|
|
1654
|
-
} else {
|
|
1655
|
-
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
const result = await client.sendEditorCommand("update_blocks", {
|
|
1659
|
-
mode: "html_replace_by_index",
|
|
1660
|
-
blockIndices,
|
|
1661
|
-
newHTML: finalHTML,
|
|
1662
|
-
insertOnly: insertOnly || false,
|
|
1663
|
-
insertPosition,
|
|
1664
|
-
}, 10000);
|
|
1665
|
-
if (!result)
|
|
1666
|
-
return timeoutResponse(name, client, args?.site);
|
|
1667
|
-
if (!result.success)
|
|
1668
|
-
return errorResponse(name, result.error, args?.site);
|
|
1669
|
-
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}` }] };
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
// newHTMLの場合
|
|
1673
|
-
if (newHTML) {
|
|
1674
|
-
const result = await client.sendEditorCommand("update_blocks", {
|
|
1675
|
-
mode: "html_replace_by_index",
|
|
1676
|
-
blockIndices,
|
|
1677
|
-
newHTML,
|
|
1678
|
-
insertOnly: insertOnly || false,
|
|
1679
|
-
insertPosition,
|
|
1680
|
-
}, 10000);
|
|
1681
|
-
if (!result)
|
|
1682
|
-
return timeoutResponse(name, client, args?.site);
|
|
1683
|
-
if (!result.success)
|
|
1684
|
-
return errorResponse(name, result.error, args?.site);
|
|
1685
|
-
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了` }] };
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
// attributeUpdatesの場合
|
|
1689
|
-
const result = await client.sendEditorCommand("update_blocks", {
|
|
1690
|
-
mode: "attribute_update_by_index",
|
|
1691
|
-
blockIndices,
|
|
1692
|
-
attributeUpdates,
|
|
1693
|
-
dryRun: dryRun || false,
|
|
1694
|
-
}, 10000);
|
|
1695
|
-
if (!result)
|
|
1696
|
-
return timeoutResponse(name, client, args?.site);
|
|
1697
|
-
if (!result.success)
|
|
1698
|
-
return errorResponse(name, result.error, args?.site);
|
|
1699
|
-
return { content: [{ type: "text", text: `✅ 属性更新完了` }] };
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
// その他のターゲット → 全てWP側で解決&実行
|
|
1703
|
-
const maxWait = (section || blockType || contains) ? 15000 : 10000;
|
|
1704
|
-
const result = await client.sendEditorCommand("update_blocks", {
|
|
1705
|
-
index, indices, startIndex, endIndex,
|
|
1706
|
-
section, blockType, typeIndex, contains,
|
|
1707
|
-
headingLevel, headingContains,
|
|
1708
|
-
replacements, newHTML, attributeUpdates,
|
|
1709
|
-
insertOnly: insertOnly || false,
|
|
1710
|
-
insertPosition,
|
|
1711
|
-
dryRun: dryRun || false,
|
|
1712
|
-
}, maxWait);
|
|
1713
|
-
if (!result)
|
|
1714
|
-
return timeoutResponse(name, client, args?.site);
|
|
1715
|
-
if (!result.success)
|
|
1716
|
-
return errorResponse(name, result.error, args?.site);
|
|
1717
|
-
|
|
1718
|
-
// 結果フォーマット
|
|
1719
|
-
if (dryRun && result.results) {
|
|
1720
|
-
const previewText = result.results
|
|
1721
|
-
.filter(r => r.preview)
|
|
1722
|
-
.map(r => {
|
|
1723
|
-
if (Array.isArray(r.preview)) {
|
|
1724
|
-
return ` [${r.index}]\n${r.preview.map(p => ` "${p.old}" → "${p.new}" (${p.count}箇所)`).join('\n')}`;
|
|
1725
|
-
} else if (r.preview?.willUpdate) {
|
|
1726
|
-
return ` [${r.index}]: 属性更新予定 ${JSON.stringify(r.preview.willUpdate)}`;
|
|
1727
|
-
} else if (r.preview?.skipped) {
|
|
1728
|
-
return ` [${r.index}]: スキップ (${r.preview.skipped})`;
|
|
1729
|
-
}
|
|
1730
|
-
return ` [${r.index}]: プレビュー不可`;
|
|
1731
|
-
})
|
|
1732
|
-
.join('\n');
|
|
1733
|
-
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}` }] };
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
if (result.results) {
|
|
1737
|
-
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
1738
|
-
const skipCount = result.results.filter(r => r.skipped).length;
|
|
1739
|
-
const failCount = result.results.filter(r => !r.success).length;
|
|
1740
|
-
const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
|
|
1741
|
-
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}` }] };
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了` }] };
|
|
2284
|
+
return await handleUpdateBlocksTool(args, name);
|
|
1745
2285
|
}
|
|
1746
2286
|
|
|
1747
2287
|
case "table_operations": {
|
|
1748
|
-
const { mode, postId, message, client } = await resolveMode(args);
|
|
2288
|
+
const { mode, postId: _postId, message, client } = await resolveMode(args);
|
|
1749
2289
|
if (mode === 'error') {
|
|
1750
2290
|
return errorResponse(name, message, args?.site);
|
|
1751
2291
|
}
|
|
2292
|
+
const _resolved = mode === 'headless' ? await resolvePostId(args?.postId, client) : { postId: _postId };
|
|
2293
|
+
if (_resolved.error) return { content: [{ type: "text", text: _resolved.error }], isError: true };
|
|
2294
|
+
const postId = _resolved.postId ?? _postId;
|
|
1752
2295
|
|
|
1753
2296
|
const { index, action } = (args || {});
|
|
1754
2297
|
if (index === undefined) {
|
|
@@ -1762,7 +2305,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1762
2305
|
index, action,
|
|
1763
2306
|
row: args.row, col: args.col,
|
|
1764
2307
|
content: args.content, position: args.position,
|
|
1765
|
-
cells: args.cells,
|
|
2308
|
+
cells: args.cells,
|
|
1766
2309
|
};
|
|
1767
2310
|
|
|
1768
2311
|
// get_structure の結果をフォーマットするヘルパー
|
|
@@ -1775,7 +2318,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1775
2318
|
for (const row of section.rows) {
|
|
1776
2319
|
const cellTexts = row.cells.map((c, ci) => {
|
|
1777
2320
|
const content = c.content.replace(/<[^>]*>/g, '').trim();
|
|
1778
|
-
|
|
2321
|
+
const flags = [];
|
|
2322
|
+
if (/<img\b/i.test(c.content)) flags.push('img');
|
|
2323
|
+
if (/<a[\s>]/i.test(c.content)) flags.push('a');
|
|
2324
|
+
if (c.style) flags.push('style');
|
|
2325
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
|
|
2326
|
+
return `[${row.globalRow},${ci}] ${content || '(空)'}${flagStr}`;
|
|
1779
2327
|
});
|
|
1780
2328
|
text += ` 行${row.globalRow}: ${cellTexts.join(' | ')}\n`;
|
|
1781
2329
|
}
|
|
@@ -1804,9 +2352,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1804
2352
|
return { content: [{ type: "text", text: `✅ ${action} 完了: ${JSON.stringify(result.detail || {})}` }] };
|
|
1805
2353
|
}
|
|
1806
2354
|
|
|
2355
|
+
case "open_in_browser": {
|
|
2356
|
+
const target = args?.target || 'editor';
|
|
2357
|
+
if (target !== 'editor' && target !== 'front') {
|
|
2358
|
+
return { content: [{ type: "text", text: `❌ target は "editor" または "front" を指定してください(指定値: "${target}")` }], isError: true };
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
let { mode, postId: _postId, message, client, errorCode } = await resolveMode(args);
|
|
2362
|
+
|
|
2363
|
+
// Editor conflict は読み取り専用なので無視して続行
|
|
2364
|
+
if (mode === 'error' && errorCode === 'editor_conflict') {
|
|
2365
|
+
mode = 'headless';
|
|
2366
|
+
} else if (mode === 'error') {
|
|
2367
|
+
return errorResponse(name, message, args?.site);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
let postId;
|
|
2371
|
+
if (mode === 'editor') {
|
|
2372
|
+
postId = _postId;
|
|
2373
|
+
} else {
|
|
2374
|
+
const resolved = await resolvePostId(args?.postId, client, { skipConflictCheck: true });
|
|
2375
|
+
if (resolved.error) return { content: [{ type: "text", text: resolved.error }], isError: true };
|
|
2376
|
+
postId = resolved.postId ?? _postId;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
let url;
|
|
2380
|
+
if (target === 'front') {
|
|
2381
|
+
const meta = await client.headlessGetMeta(postId);
|
|
2382
|
+
url = meta.link;
|
|
2383
|
+
} else {
|
|
2384
|
+
url = `${client.wpUrl.replace(/\/$/, '')}/wp-admin/post.php?post=${postId}&action=edit`;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
if (!isValidHttpUrl(url)) {
|
|
2388
|
+
return { content: [{ type: "text", text: `❌ 無効なURLです: ${url}` }], isError: true };
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
try {
|
|
2392
|
+
await openInBrowser(url);
|
|
2393
|
+
const label = target === 'front' ? 'フロントページ' : 'エディタ';
|
|
2394
|
+
return { content: [{ type: "text", text: `🌐 ${label}をブラウザで開きました\nURL: ${url}` }] };
|
|
2395
|
+
} catch (e) {
|
|
2396
|
+
return { content: [{ type: "text", text: `⚠️ ブラウザの起動に失敗しました。URLを手動で開いてください:\n${url}\n\nエラー: ${e.message}` }], isError: true };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
1807
2400
|
case "list_connections": {
|
|
1808
2401
|
const conns = registry.list();
|
|
1809
|
-
const text = conns.map(c =>
|
|
2402
|
+
const text = conns.map(c => {
|
|
2403
|
+
const mark = c.isDefault ? ' (default)' : '';
|
|
2404
|
+
return ` ${c.name}${mark}: ${c.url} (${c.user})`;
|
|
2405
|
+
}).join('\n');
|
|
1810
2406
|
return { content: [{ type: "text", text: `🔗 接続一覧 (${conns.length}件)\n\n${text}` }] };
|
|
1811
2407
|
}
|
|
1812
2408
|
|