friday-mcp-v2 2.0.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 +1861 -0
- package/dist/wordpress-api.js +439 -0
- package/package.json +23 -0
|
@@ -0,0 +1,1861 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* F.R.I.D.A.Y. MCP Server v2.0
|
|
4
|
+
* WordPress REST API (HTTPS, App Password) 経由で直接通信
|
|
5
|
+
* Bridge Server / SharedState 不要
|
|
6
|
+
* Headless モード対応: エディタ未接続時は parse_blocks/serialize_blocks で直接操作
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { FridayWPClient, ConnectionRegistry } from "./wordpress-api.js";
|
|
12
|
+
import fetch from "node-fetch";
|
|
13
|
+
|
|
14
|
+
const registry = new ConnectionRegistry();
|
|
15
|
+
|
|
16
|
+
// ヘルパー関数: 正規表現用に文字列をエスケープ
|
|
17
|
+
function escapeRegExp(string) {
|
|
18
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Editor/Headless モード判定ヘルパー
|
|
23
|
+
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
24
|
+
*/
|
|
25
|
+
async function resolveMode(args) {
|
|
26
|
+
let client;
|
|
27
|
+
try {
|
|
28
|
+
client = registry.get(args?.site);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return { mode: 'error', message: e.message };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const status = await client.getStatus();
|
|
35
|
+
if (status.editorConnected && status.postId) {
|
|
36
|
+
const argPostId = args?.postId;
|
|
37
|
+
if (argPostId && argPostId !== status.postId) {
|
|
38
|
+
return { mode: 'headless', postId: argPostId, client };
|
|
39
|
+
}
|
|
40
|
+
return { mode: 'editor', postId: status.postId, client };
|
|
41
|
+
}
|
|
42
|
+
const postId = args?.postId;
|
|
43
|
+
if (!postId) {
|
|
44
|
+
return { mode: 'error', message: 'エディタ未接続です。postId を指定して Headless モードを使用してください。' };
|
|
45
|
+
}
|
|
46
|
+
return { mode: 'headless', postId, client };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
const postId = args?.postId;
|
|
49
|
+
if (postId) {
|
|
50
|
+
return { mode: 'headless', postId, client };
|
|
51
|
+
}
|
|
52
|
+
return { mode: 'error', message: `接続エラー: ${e.message}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// postId パラメータ定義(Headless 対応ツール共通)
|
|
57
|
+
const postIdParam = {
|
|
58
|
+
type: "number",
|
|
59
|
+
description: "投稿ID(エディタ未接続時は必須)",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// site パラメータ定義(マルチ接続共通)
|
|
63
|
+
const siteParam = {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "接続名(複数サイト/ユーザー設定時に指定。省略でデフォルト接続)",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// フィードバック送信ヘルパー
|
|
69
|
+
const FEEDBACK_URL = process.env.FRIDAY_FEEDBACK_URL || '';
|
|
70
|
+
async function sendFeedback(data) {
|
|
71
|
+
if (!FEEDBACK_URL) return;
|
|
72
|
+
try {
|
|
73
|
+
await fetch(FEEDBACK_URL, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ ...data, version: '2.0.0' }),
|
|
77
|
+
});
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// フィードバック送信失敗は無視(本体の動作に影響させない)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function errorResponse(toolName, message, site) {
|
|
83
|
+
sendFeedback({ tool: toolName, category: 'error', content: message, site: site || 'default' });
|
|
84
|
+
return { content: [{ type: "text", text: `❌ ${message}` }], isError: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MCPサーバーの定義
|
|
88
|
+
const server = new Server({
|
|
89
|
+
name: "friday-mcp-v2",
|
|
90
|
+
version: "2.0.0",
|
|
91
|
+
}, {
|
|
92
|
+
capabilities: {
|
|
93
|
+
tools: {},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 利用可能なツールのリスト
|
|
98
|
+
const tools = [
|
|
99
|
+
{
|
|
100
|
+
name: "get_article_structure",
|
|
101
|
+
description: "Get article structure (supports progressive loading: headings only by default, use parameters to zoom in)",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
postId: postIdParam,
|
|
106
|
+
site: siteParam,
|
|
107
|
+
blockType: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Filter by block type (e.g. core/table) - shows all blocks of this type across sections",
|
|
110
|
+
},
|
|
111
|
+
section: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Zoom into specific section - shows all blocks in that section with details",
|
|
114
|
+
},
|
|
115
|
+
headingLevel: {
|
|
116
|
+
type: "number",
|
|
117
|
+
description: "Filter headings by level (2, 3, 4) - shows only headings of this level",
|
|
118
|
+
},
|
|
119
|
+
expand: {
|
|
120
|
+
type: "number",
|
|
121
|
+
description: "Container block index to expand (shows child blocks)",
|
|
122
|
+
},
|
|
123
|
+
contains: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Search text in block content - returns all matching blocks",
|
|
126
|
+
},
|
|
127
|
+
full: {
|
|
128
|
+
type: "boolean",
|
|
129
|
+
description: "Get full block data with all attributes (use with limit/offset for large articles)",
|
|
130
|
+
},
|
|
131
|
+
limit: {
|
|
132
|
+
type: "number",
|
|
133
|
+
description: "Maximum number of blocks to return (for pagination with full=true)",
|
|
134
|
+
},
|
|
135
|
+
offset: {
|
|
136
|
+
type: "number",
|
|
137
|
+
description: "Starting block index for pagination (for pagination with full=true)",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "select_block",
|
|
144
|
+
description: "Select block(s) in WordPress editor. Index is 0-based flattened block position (including nested blocks).",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
site: siteParam,
|
|
149
|
+
index: {
|
|
150
|
+
type: "number",
|
|
151
|
+
description: "Block index (0-based, flattened including nested blocks)",
|
|
152
|
+
},
|
|
153
|
+
blockType: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Block type (e.g. core/table)",
|
|
156
|
+
},
|
|
157
|
+
typeIndex: {
|
|
158
|
+
type: "number",
|
|
159
|
+
description: "N-th block of the type (0-based)",
|
|
160
|
+
},
|
|
161
|
+
contains: {
|
|
162
|
+
type: "string",
|
|
163
|
+
description: "Search text in block content",
|
|
164
|
+
},
|
|
165
|
+
startIndex: {
|
|
166
|
+
type: "number",
|
|
167
|
+
description: "Range selection start",
|
|
168
|
+
},
|
|
169
|
+
endIndex: {
|
|
170
|
+
type: "number",
|
|
171
|
+
description: "Range selection end",
|
|
172
|
+
},
|
|
173
|
+
headingLevel: {
|
|
174
|
+
type: "number",
|
|
175
|
+
description: "Heading level for section selection (2, 3, 4)",
|
|
176
|
+
},
|
|
177
|
+
headingContains: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Heading text for section selection",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "delete_block",
|
|
186
|
+
description: "Delete block(s) from the editor. Removes the block at the specified index or the currently selected block.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
postId: postIdParam,
|
|
191
|
+
site: siteParam,
|
|
192
|
+
index: { type: "number", description: "Block index to delete (0-based). If omitted, deletes selected block." },
|
|
193
|
+
count: { type: "number", description: "Number of consecutive blocks to delete (default: 1)" },
|
|
194
|
+
indices: { type: "array", items: { type: "number" }, description: "Multiple block indices to delete (0-based). Cannot be used with index/count." },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "move_block",
|
|
200
|
+
description: "Move a block to a different position. Use from/to for top-level blocks, or fromFlat/toFlat for any block (including nested).",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
postId: postIdParam,
|
|
205
|
+
site: siteParam,
|
|
206
|
+
from: { type: "number", description: "Source top-level block index (0-based)" },
|
|
207
|
+
to: { type: "number", description: "Target position (0-based, can be length for end)" },
|
|
208
|
+
fromFlat: { type: "number", description: "Source flattened block index (0-based, supports nested blocks)" },
|
|
209
|
+
toFlat: { type: "number", description: "Target flattened position (pre-move basis, can be blockCount for end)" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "undo",
|
|
215
|
+
description: "Undo the last editor action.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
site: siteParam,
|
|
220
|
+
steps: { type: "number", description: "Number of undo steps (default: 1)" },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "redo",
|
|
226
|
+
description: "Redo the last undone action.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
site: siteParam,
|
|
231
|
+
steps: { type: "number", description: "Number of redo steps (default: 1)" },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "duplicate_block",
|
|
237
|
+
description: "Duplicate a block. Duplicates the block at the specified index or the currently selected block.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
postId: postIdParam,
|
|
242
|
+
site: siteParam,
|
|
243
|
+
index: { type: "number", description: "Block index to duplicate (0-based). If omitted, duplicates selected block." },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "save_post",
|
|
249
|
+
description: "Save the current post (preserves current status).",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
postId: postIdParam,
|
|
254
|
+
site: siteParam,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "get_post_meta",
|
|
260
|
+
description: "Get post metadata (title, status, categories, tags, etc.).",
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
postId: postIdParam,
|
|
265
|
+
site: siteParam,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "update_post_meta",
|
|
271
|
+
description: "Update post metadata (title, status, categories, tags, slug, excerpt, featured image).",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
postId: postIdParam,
|
|
276
|
+
site: siteParam,
|
|
277
|
+
title: { type: "string", description: "Post title" },
|
|
278
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Post status" },
|
|
279
|
+
slug: { type: "string", description: "Post slug (URL)" },
|
|
280
|
+
categories: { type: "array", items: { type: "number" }, description: "Category IDs" },
|
|
281
|
+
tags: { type: "array", items: { type: "number" }, description: "Tag IDs" },
|
|
282
|
+
excerpt: { type: "string", description: "Post excerpt" },
|
|
283
|
+
featured_media: { type: "number", description: "Featured image media ID (0 to remove)" },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "list_posts",
|
|
289
|
+
description: "Search and list WordPress posts. Use to find postId for Headless mode operations. Default returns only published posts; use status='any' to include drafts. Search matches both title and content; use short single keywords for best results.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
site: siteParam,
|
|
294
|
+
search: { type: "string", description: "Search keyword (title/content)" },
|
|
295
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private", "any"], description: "Filter by status (default: publish)" },
|
|
296
|
+
categories: { type: "array", items: { type: "number" }, description: "Category IDs to filter by" },
|
|
297
|
+
tags: { type: "array", items: { type: "number" }, description: "Tag IDs to filter by" },
|
|
298
|
+
per_page: { type: "integer", description: "Results per page (1-100, default: 10)", minimum: 1, maximum: 100 },
|
|
299
|
+
page: { type: "integer", description: "Page number (default: 1)", minimum: 1 },
|
|
300
|
+
orderby: { type: "string", enum: ["date", "title", "modified", "id"], description: "Sort field (default: date)" },
|
|
301
|
+
order: { type: "string", enum: ["asc", "desc"], description: "Sort order (default: desc)" },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "list_taxonomies",
|
|
307
|
+
description: "List WordPress categories or tags. Use to find category/tag IDs for update_post_meta or list_posts filtering.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
site: siteParam,
|
|
312
|
+
type: { type: "string", enum: ["category", "tag"], description: "Taxonomy type (required)" },
|
|
313
|
+
search: { type: "string", description: "Search by name" },
|
|
314
|
+
per_page: { type: "integer", description: "Results per page (1-100, default: 10)", minimum: 1, maximum: 100 },
|
|
315
|
+
page: { type: "integer", description: "Page number (default: 1)", minimum: 1 },
|
|
316
|
+
orderby: { type: "string", enum: ["name", "id", "count", "slug"], description: "Sort field (default: name)" },
|
|
317
|
+
order: { type: "string", enum: ["asc", "desc"], description: "Sort order (default: asc)" },
|
|
318
|
+
hide_empty: { type: "boolean", description: "Hide terms with no posts (default: false)" },
|
|
319
|
+
},
|
|
320
|
+
required: ["type"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "insert_block",
|
|
325
|
+
description: "Insert a new block at the specified position. If index is omitted, inserts at the end.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
postId: postIdParam,
|
|
330
|
+
site: siteParam,
|
|
331
|
+
blockType: { type: "string", description: "Block type (e.g. core/paragraph, core/heading, core/list)" },
|
|
332
|
+
content: { type: "string", description: "Block content (HTML or plain text depending on block type)" },
|
|
333
|
+
index: { type: "number", description: "Insert position (0-based flattened index). If omitted, appends to end." },
|
|
334
|
+
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." },
|
|
335
|
+
rawHTML: { type: "string", description: "Gutenberg markup HTML to insert directly. Cannot be used with blockType/content or blocks." },
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "get_selection",
|
|
341
|
+
description: "Get lightweight info about the block currently selected by the user in WordPress editor. Returns type, index, and cursor-selected text only.",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
site: siteParam,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "get_block_html",
|
|
351
|
+
description: "Get serialized Gutenberg HTML (block comments included) for specified blocks. Use this before update_blocks with newHTML to safely edit custom/theme blocks.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
postId: postIdParam,
|
|
356
|
+
site: siteParam,
|
|
357
|
+
target: {
|
|
358
|
+
type: "string",
|
|
359
|
+
enum: ["selected"],
|
|
360
|
+
description: "Use 'selected' to target the block currently selected by the user in WordPress editor.",
|
|
361
|
+
},
|
|
362
|
+
index: {
|
|
363
|
+
type: "number",
|
|
364
|
+
description: "Single block index (0-based flattened).",
|
|
365
|
+
},
|
|
366
|
+
indices: {
|
|
367
|
+
type: "array",
|
|
368
|
+
items: { type: "number" },
|
|
369
|
+
description: "Multiple block indices (0-based flattened).",
|
|
370
|
+
},
|
|
371
|
+
startIndex: {
|
|
372
|
+
type: "number",
|
|
373
|
+
description: "Range selection start index.",
|
|
374
|
+
},
|
|
375
|
+
endIndex: {
|
|
376
|
+
type: "number",
|
|
377
|
+
description: "Range selection end index.",
|
|
378
|
+
},
|
|
379
|
+
section: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: "Target section by heading text (partial match). Combines with blockType.",
|
|
382
|
+
},
|
|
383
|
+
blockType: {
|
|
384
|
+
type: "string",
|
|
385
|
+
description: "Filter by block type (e.g. core/table). Combines with section.",
|
|
386
|
+
},
|
|
387
|
+
typeIndex: {
|
|
388
|
+
type: "number",
|
|
389
|
+
description: "N-th block of the specified blockType (0-based). Use with blockType.",
|
|
390
|
+
},
|
|
391
|
+
headingLevel: {
|
|
392
|
+
type: "number",
|
|
393
|
+
description: "Heading level for section selection (2, 3, 4). Use with headingContains.",
|
|
394
|
+
},
|
|
395
|
+
headingContains: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Heading text for section selection. Use with headingLevel.",
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "update_blocks",
|
|
404
|
+
description: "Update block(s) in WordPress editor. Combines targeting and modification in a single call. Supports single/multi/filter-based targeting with replacements, full HTML replace, or attribute updates.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
postId: postIdParam,
|
|
409
|
+
site: siteParam,
|
|
410
|
+
target: {
|
|
411
|
+
type: "string",
|
|
412
|
+
enum: ["selected"],
|
|
413
|
+
description: "Use 'selected' to target the block currently selected by the user in WordPress editor.",
|
|
414
|
+
},
|
|
415
|
+
index: {
|
|
416
|
+
type: "number",
|
|
417
|
+
description: "Single block index (0-based flattened).",
|
|
418
|
+
},
|
|
419
|
+
indices: {
|
|
420
|
+
type: "array",
|
|
421
|
+
items: { type: "number" },
|
|
422
|
+
description: "Multiple block indices (0-based flattened).",
|
|
423
|
+
},
|
|
424
|
+
startIndex: {
|
|
425
|
+
type: "number",
|
|
426
|
+
description: "Range selection start index.",
|
|
427
|
+
},
|
|
428
|
+
endIndex: {
|
|
429
|
+
type: "number",
|
|
430
|
+
description: "Range selection end index.",
|
|
431
|
+
},
|
|
432
|
+
section: {
|
|
433
|
+
type: "string",
|
|
434
|
+
description: "Target section by heading text (partial match). Combines with blockType/contains.",
|
|
435
|
+
},
|
|
436
|
+
blockType: {
|
|
437
|
+
type: "string",
|
|
438
|
+
description: "Filter by block type (e.g. core/table). Combines with section/contains.",
|
|
439
|
+
},
|
|
440
|
+
typeIndex: {
|
|
441
|
+
type: "number",
|
|
442
|
+
description: "N-th block of the specified blockType (0-based). Use with blockType.",
|
|
443
|
+
},
|
|
444
|
+
contains: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Target blocks containing this text.",
|
|
447
|
+
},
|
|
448
|
+
headingLevel: {
|
|
449
|
+
type: "number",
|
|
450
|
+
description: "Heading level for section selection (2, 3, 4). Use with headingContains.",
|
|
451
|
+
},
|
|
452
|
+
headingContains: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "Heading text for section selection. Use with headingLevel.",
|
|
455
|
+
},
|
|
456
|
+
replacements: {
|
|
457
|
+
type: "array",
|
|
458
|
+
items: {
|
|
459
|
+
type: "object",
|
|
460
|
+
properties: {
|
|
461
|
+
old: { type: "string", description: "String to replace" },
|
|
462
|
+
new: { type: "string", description: "Replacement string" },
|
|
463
|
+
regex: { type: "boolean", description: "Treat 'old' as a regular expression pattern (default: false)" },
|
|
464
|
+
},
|
|
465
|
+
required: ["old", "new"],
|
|
466
|
+
},
|
|
467
|
+
description: "Diff-mode text replacements. Applied to serialized HTML of each target block.",
|
|
468
|
+
},
|
|
469
|
+
newHTML: {
|
|
470
|
+
type: "string",
|
|
471
|
+
description: "Full HTML replacement (Gutenberg markup). Only for contiguous targets (index/startIndex+endIndex/target:selected).",
|
|
472
|
+
},
|
|
473
|
+
attributeUpdates: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
filter: { type: "object", description: "Only update blocks matching these attributes (e.g. {alt: ''})" },
|
|
477
|
+
set: { type: "object", description: "Attributes to set (e.g. {alt: 'new alt text'})" },
|
|
478
|
+
},
|
|
479
|
+
description: "Direct attribute updates via updateBlockAttributes API.",
|
|
480
|
+
},
|
|
481
|
+
insertOnly: {
|
|
482
|
+
type: "boolean",
|
|
483
|
+
description: "Keep existing blocks, insert newHTML as new blocks. Only with newHTML.",
|
|
484
|
+
},
|
|
485
|
+
insertPosition: {
|
|
486
|
+
type: ["number", "string"],
|
|
487
|
+
description: "Insert position when insertOnly=true. 'before', 'after' (default), or number.",
|
|
488
|
+
},
|
|
489
|
+
dryRun: {
|
|
490
|
+
type: "boolean",
|
|
491
|
+
description: "Preview changes without applying.",
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "table_operations",
|
|
498
|
+
description: "Perform structured operations on core/table blocks (get structure, update cells, add/delete rows/columns, style cells/rows).",
|
|
499
|
+
inputSchema: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
postId: postIdParam,
|
|
503
|
+
site: siteParam,
|
|
504
|
+
index: {
|
|
505
|
+
type: "number",
|
|
506
|
+
description: "Table block's flattened index (0-based).",
|
|
507
|
+
},
|
|
508
|
+
action: {
|
|
509
|
+
type: "string",
|
|
510
|
+
enum: ["get_structure", "update_cell", "add_row", "delete_row", "add_column", "delete_column", "style_cell", "style_row"],
|
|
511
|
+
description: "Operation type.",
|
|
512
|
+
},
|
|
513
|
+
row: {
|
|
514
|
+
type: "number",
|
|
515
|
+
description: "Row index (0-based, counting through thead/tbody/tfoot).",
|
|
516
|
+
},
|
|
517
|
+
col: {
|
|
518
|
+
type: "number",
|
|
519
|
+
description: "Column index (0-based).",
|
|
520
|
+
},
|
|
521
|
+
content: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "Cell content (HTML allowed). Used by update_cell.",
|
|
524
|
+
},
|
|
525
|
+
position: {
|
|
526
|
+
type: "number",
|
|
527
|
+
description: "Insert position for add_row/add_column. Omit for end.",
|
|
528
|
+
},
|
|
529
|
+
cells: {
|
|
530
|
+
type: "array",
|
|
531
|
+
items: { type: "string" },
|
|
532
|
+
description: "Cell contents for new row (add_row). Omit for empty cells.",
|
|
533
|
+
},
|
|
534
|
+
style: {
|
|
535
|
+
type: "object",
|
|
536
|
+
properties: {
|
|
537
|
+
backgroundColor: { type: "string", description: "Background color (e.g. #ff0000)" },
|
|
538
|
+
color: { type: "string", description: "Text color" },
|
|
539
|
+
fontWeight: { type: "string", description: "Font weight (bold, normal)" },
|
|
540
|
+
textAlign: { type: "string", description: "Text alignment (left, center, right)" },
|
|
541
|
+
fontSize: { type: "string", description: "Font size (e.g. 14px)" },
|
|
542
|
+
},
|
|
543
|
+
description: "Style for style_cell/style_row.",
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
required: ["index", "action"],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "list_connections",
|
|
551
|
+
description: "List available site/user connections.",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "report_feedback",
|
|
559
|
+
description: "Report feedback, issues, or suggestions about MCP tools to the developer. Only use when the user explicitly requests to send feedback. Never use automatically without user consent.",
|
|
560
|
+
inputSchema: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
tool: {
|
|
564
|
+
type: "string",
|
|
565
|
+
description: "Tool name that the feedback is about (e.g. update_blocks, get_article_structure)",
|
|
566
|
+
},
|
|
567
|
+
category: {
|
|
568
|
+
type: "string",
|
|
569
|
+
enum: ["error", "feedback", "suggestion"],
|
|
570
|
+
description: "Type of feedback: error (bug/failure), feedback (usability issue), suggestion (improvement idea)",
|
|
571
|
+
},
|
|
572
|
+
content: {
|
|
573
|
+
type: "string",
|
|
574
|
+
description: "Detailed description of the issue or suggestion",
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
required: ["content"],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
// ツールリストのハンドラ
|
|
583
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
584
|
+
return { tools };
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ツール実行のハンドラ
|
|
588
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
589
|
+
const { name, arguments: args } = request.params;
|
|
590
|
+
try {
|
|
591
|
+
switch (name) {
|
|
592
|
+
case "get_article_structure": {
|
|
593
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
594
|
+
if (mode === 'error') {
|
|
595
|
+
return errorResponse(name, message, args?.site);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const { blockType, section, headingLevel, expand, contains, full, limit, offset } = (args || {});
|
|
599
|
+
|
|
600
|
+
// 使用パラメータを特定
|
|
601
|
+
const usedParams = [];
|
|
602
|
+
if (section) usedParams.push("section");
|
|
603
|
+
if (blockType) usedParams.push("blockType");
|
|
604
|
+
if (headingLevel) usedParams.push("headingLevel");
|
|
605
|
+
if (expand !== undefined) usedParams.push("expand");
|
|
606
|
+
if (contains) usedParams.push("contains");
|
|
607
|
+
if (full) usedParams.push("full");
|
|
608
|
+
if (limit && limit > 0) usedParams.push(`limit=${limit}`);
|
|
609
|
+
if (offset && offset > 0) usedParams.push(`offset=${offset}`);
|
|
610
|
+
const paramLabel = usedParams.length > 0 ? usedParams.join(" + ") : "default";
|
|
611
|
+
|
|
612
|
+
// Headless / Editor でデータ取得を分岐
|
|
613
|
+
let state;
|
|
614
|
+
if (mode === 'headless') {
|
|
615
|
+
state = await client.headlessGetStructure(postId);
|
|
616
|
+
// full/contains の場合は allBlocks が必要
|
|
617
|
+
if (full || contains) {
|
|
618
|
+
const blocksData = await client.headlessGetBlocks(postId);
|
|
619
|
+
state.allBlocks = blocksData.blocks;
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
state = await client.getEditorState();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ========================================
|
|
626
|
+
// 全ブロック情報取得(full=true)
|
|
627
|
+
// ========================================
|
|
628
|
+
if (full) {
|
|
629
|
+
if (!state.allBlocks || state.allBlocks.length === 0) {
|
|
630
|
+
return {
|
|
631
|
+
content: [{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: `📊 get_article_structure(${paramLabel})\n\n⚠️ 全ブロック情報がありません。${mode === 'editor' ? 'WordPressエディタで記事を開き直してください。' : '記事が見つかりません。'}`,
|
|
634
|
+
}],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let blocks = state.allBlocks;
|
|
639
|
+
|
|
640
|
+
if (blockType) {
|
|
641
|
+
blocks = blocks.filter(b => b.type === blockType);
|
|
642
|
+
}
|
|
643
|
+
if (section) {
|
|
644
|
+
blocks = blocks.filter(b => b.section === section || b.section?.includes(section));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const totalCount = blocks.length;
|
|
648
|
+
|
|
649
|
+
const startIdx = (offset && offset > 0) ? offset : 0;
|
|
650
|
+
const endIdx = (limit && limit > 0) ? startIdx + limit : blocks.length;
|
|
651
|
+
blocks = blocks.slice(startIdx, endIdx);
|
|
652
|
+
|
|
653
|
+
const blockList = blocks.map(b => {
|
|
654
|
+
const attrPreview = JSON.stringify(b.attributes || {}).slice(0, 200);
|
|
655
|
+
return ` [${b.index}] ${b.type} - ${b.section || "(記事冒頭)"} (d:${b.depth})\n ${attrPreview}${attrPreview.length >= 200 ? '...' : ''}`;
|
|
656
|
+
}).join("\n\n");
|
|
657
|
+
|
|
658
|
+
let paginationInfo = '';
|
|
659
|
+
if (startIdx > 0 || endIdx < totalCount) {
|
|
660
|
+
paginationInfo = `\n\n📄 表示: ${startIdx}〜${Math.min(endIdx, totalCount) - 1} / 全${totalCount}件`;
|
|
661
|
+
if (endIdx < totalCount) {
|
|
662
|
+
paginationInfo += `\n 次のページ: offset=${endIdx}`;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
content: [{
|
|
668
|
+
type: "text",
|
|
669
|
+
text: `📊 get_article_structure(${paramLabel})\n\n全ブロック情報 (${blocks.length}件):\n\n${blockList}${paginationInfo}`,
|
|
670
|
+
}],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ========================================
|
|
675
|
+
// コンテンツ検索
|
|
676
|
+
// ========================================
|
|
677
|
+
if (contains) {
|
|
678
|
+
// allBlocks がある場合は MCP 側でフィルタ(高速)
|
|
679
|
+
if (state.allBlocks && state.allBlocks.length > 0) {
|
|
680
|
+
let matches = [];
|
|
681
|
+
for (const block of state.allBlocks) {
|
|
682
|
+
if (blockType && block.type !== blockType) continue;
|
|
683
|
+
const attrJson = JSON.stringify(block.attributes || {});
|
|
684
|
+
const htmlStr = block.html || '';
|
|
685
|
+
// attributes → html の順で検索し、ヒット元を特定
|
|
686
|
+
let hitSource = null;
|
|
687
|
+
let hitIdx = -1;
|
|
688
|
+
if (attrJson.includes(contains)) {
|
|
689
|
+
hitSource = attrJson;
|
|
690
|
+
hitIdx = attrJson.indexOf(contains);
|
|
691
|
+
} else if (htmlStr.includes(contains)) {
|
|
692
|
+
hitSource = htmlStr;
|
|
693
|
+
hitIdx = htmlStr.indexOf(contains);
|
|
694
|
+
}
|
|
695
|
+
if (hitSource && hitIdx >= 0) {
|
|
696
|
+
const start = Math.max(0, hitIdx - 20);
|
|
697
|
+
const end = Math.min(hitSource.length, hitIdx + contains.length + 20);
|
|
698
|
+
let preview = hitSource.slice(start, end);
|
|
699
|
+
if (start > 0) preview = '...' + preview;
|
|
700
|
+
if (end < hitSource.length) preview = preview + '...';
|
|
701
|
+
matches.push({
|
|
702
|
+
index: block.index,
|
|
703
|
+
type: block.type,
|
|
704
|
+
section: block.section,
|
|
705
|
+
depth: block.depth,
|
|
706
|
+
preview: preview
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (matches.length === 0) {
|
|
712
|
+
return {
|
|
713
|
+
content: [{
|
|
714
|
+
type: "text",
|
|
715
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロックは見つかりませんでした。`,
|
|
716
|
+
}],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const matchList = matches
|
|
721
|
+
.map(m => {
|
|
722
|
+
let line = ` index ${m.index}: ${m.type} - ${m.section || "(記事冒頭)"} (d:${m.depth})`;
|
|
723
|
+
if (m.preview) {
|
|
724
|
+
line += `\n "${m.preview}"`;
|
|
725
|
+
}
|
|
726
|
+
return line;
|
|
727
|
+
})
|
|
728
|
+
.join("\n");
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
content: [{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${matches.length}個):\n\n${matchList}`,
|
|
734
|
+
}],
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// allBlocks がない場合 — Editor モードではオンデマンド検索
|
|
739
|
+
if (mode === 'editor') {
|
|
740
|
+
const searchResult = await client.searchBlocks(contains, blockType);
|
|
741
|
+
|
|
742
|
+
if (!searchResult) {
|
|
743
|
+
return {
|
|
744
|
+
content: [{
|
|
745
|
+
type: "text",
|
|
746
|
+
text: `⏳ 検索タイムアウト: WordPressエディタが応答していません。`,
|
|
747
|
+
}],
|
|
748
|
+
};
|
|
749
|
+
sendFeedback({ tool: name, category: 'error', content: '検索タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' });
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (searchResult.matches && searchResult.matches.length === 0) {
|
|
753
|
+
return {
|
|
754
|
+
content: [{
|
|
755
|
+
type: "text",
|
|
756
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロックは見つかりませんでした。`,
|
|
757
|
+
}],
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const matchList = (searchResult.matches || [])
|
|
762
|
+
.map(m => {
|
|
763
|
+
let line = ` index ${m.index}: ${m.type} - ${m.section || "(記事冒頭)"} (d:${m.depth})`;
|
|
764
|
+
if (m.preview) {
|
|
765
|
+
line += `\n "${m.preview}"`;
|
|
766
|
+
}
|
|
767
|
+
return line;
|
|
768
|
+
})
|
|
769
|
+
.join("\n");
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
content: [{
|
|
773
|
+
type: "text",
|
|
774
|
+
text: `📊 get_article_structure(${paramLabel})\n\n"${contains}" を含むブロック (${(searchResult.matches || []).length}個):\n\n${matchList}`,
|
|
775
|
+
}],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Headless で allBlocks なし — ありえないが安全ガード
|
|
780
|
+
return {
|
|
781
|
+
content: [{
|
|
782
|
+
type: "text",
|
|
783
|
+
text: `📊 get_article_structure(${paramLabel})\n\n⚠️ ブロック情報を取得できませんでした。`,
|
|
784
|
+
}],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 見出し情報がない場合
|
|
789
|
+
if (!state.headings || state.headings.length === 0) {
|
|
790
|
+
return {
|
|
791
|
+
content: [{
|
|
792
|
+
type: "text",
|
|
793
|
+
text: `📊 get_article_structure(${paramLabel})\n\n記事構造がありません。${mode === 'editor' ? 'WordPressエディタで記事を開いてください。' : '記事が見つかりません。'}`,
|
|
794
|
+
}],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ========================================
|
|
799
|
+
// セクション + blockType の組み合わせ
|
|
800
|
+
// ========================================
|
|
801
|
+
if (section && blockType && state.blockSummary) {
|
|
802
|
+
let sectionHeading = state.headings.find(h => h.text === section);
|
|
803
|
+
if (!sectionHeading) {
|
|
804
|
+
const candidates = state.headings.filter(h => h.text.includes(section));
|
|
805
|
+
if (candidates.length === 1) sectionHeading = candidates[0];
|
|
806
|
+
}
|
|
807
|
+
if (!sectionHeading) {
|
|
808
|
+
return {
|
|
809
|
+
content: [{
|
|
810
|
+
type: "text",
|
|
811
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ "${section}" に一致するセクションが見つかりません。`,
|
|
812
|
+
}],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const sectionIdx = state.headings.indexOf(sectionHeading);
|
|
817
|
+
let nextSectionBlockIndex = null;
|
|
818
|
+
for (let i = sectionIdx + 1; i < state.headings.length; i++) {
|
|
819
|
+
if (state.headings[i].level <= sectionHeading.level) {
|
|
820
|
+
nextSectionBlockIndex = state.headings[i].index;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const startIdx = sectionHeading.index;
|
|
825
|
+
const endIdx = nextSectionBlockIndex !== null ? nextSectionBlockIndex : Infinity;
|
|
826
|
+
|
|
827
|
+
if (!state.blockSummary[blockType]) {
|
|
828
|
+
return {
|
|
829
|
+
content: [{
|
|
830
|
+
type: "text",
|
|
831
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ ${blockType} は "${sectionHeading.text}" セクション内に存在しません。`,
|
|
832
|
+
}],
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const filteredBlocks = state.blockSummary[blockType].blocks
|
|
836
|
+
.filter(b => b.index > startIdx && b.index < endIdx);
|
|
837
|
+
|
|
838
|
+
if (filteredBlocks.length === 0) {
|
|
839
|
+
return {
|
|
840
|
+
content: [{
|
|
841
|
+
type: "text",
|
|
842
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ ${blockType} は "${sectionHeading.text}" セクション内に存在しません。`,
|
|
843
|
+
}],
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const blockList = filteredBlocks
|
|
848
|
+
.map(b => ` index ${b.index}: d:${b.depth || 0}`)
|
|
849
|
+
.join("\n");
|
|
850
|
+
return {
|
|
851
|
+
content: [{
|
|
852
|
+
type: "text",
|
|
853
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション内の ${blockType} (${filteredBlocks.length}個):\n\n${blockList}`,
|
|
854
|
+
}],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ========================================
|
|
859
|
+
// 特定ブロックタイプでフィルタ
|
|
860
|
+
// ========================================
|
|
861
|
+
if (blockType) {
|
|
862
|
+
if (!state.blockSummary || !state.blockSummary[blockType]) {
|
|
863
|
+
return {
|
|
864
|
+
content: [{
|
|
865
|
+
type: "text",
|
|
866
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ ${blockType} は記事内に存在しません。`,
|
|
867
|
+
}],
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const typeData = state.blockSummary[blockType];
|
|
871
|
+
const blockList = typeData.blocks
|
|
872
|
+
.map(b => {
|
|
873
|
+
let info = ` index ${b.index}: ${b.section || "(記事冒頭)"} (H${b.sectionLevel || "-"}) d:${b.depth || 0}`;
|
|
874
|
+
if (b.tabNames && b.tabNames.length > 0) {
|
|
875
|
+
info += ` [${b.tabNames.join(", ")}]`;
|
|
876
|
+
} else if (b.columnCount) {
|
|
877
|
+
info += ` [${b.columnCount}カラム]`;
|
|
878
|
+
}
|
|
879
|
+
return info;
|
|
880
|
+
})
|
|
881
|
+
.join("\n");
|
|
882
|
+
return {
|
|
883
|
+
content: [{
|
|
884
|
+
type: "text",
|
|
885
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${blockType} の一覧 (${typeData.count}個):\n\n${blockList}`,
|
|
886
|
+
}],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ========================================
|
|
891
|
+
// 特定セクションでフィルタ
|
|
892
|
+
// ========================================
|
|
893
|
+
if (section && state.blockSummary) {
|
|
894
|
+
let sectionHeading = state.headings.find(h => h.text === section);
|
|
895
|
+
if (!sectionHeading) {
|
|
896
|
+
const candidates = state.headings.filter(h => h.text.includes(section));
|
|
897
|
+
if (candidates.length === 0) {
|
|
898
|
+
return {
|
|
899
|
+
content: [{
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ "${section}" に一致するセクションが見つかりません。`,
|
|
902
|
+
}],
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (candidates.length > 1) {
|
|
906
|
+
const candidateList = candidates
|
|
907
|
+
.map(h => ` ${h.text} (H${h.level}, index:${h.index})`)
|
|
908
|
+
.join("\n");
|
|
909
|
+
return {
|
|
910
|
+
content: [{
|
|
911
|
+
type: "text",
|
|
912
|
+
text: `📊 get_article_structure(${paramLabel})\n\n⚠️ "${section}" に複数のセクションが一致しました。より具体的に指定してください:\n\n${candidateList}`,
|
|
913
|
+
}],
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
sectionHeading = candidates[0];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const sectionIdx = state.headings.indexOf(sectionHeading);
|
|
920
|
+
let nextSectionBlockIndex = null;
|
|
921
|
+
for (let i = sectionIdx + 1; i < state.headings.length; i++) {
|
|
922
|
+
if (state.headings[i].level <= sectionHeading.level) {
|
|
923
|
+
nextSectionBlockIndex = state.headings[i].index;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const sectionBlocks = [];
|
|
929
|
+
const startIdx = sectionHeading.index;
|
|
930
|
+
const endIdx = nextSectionBlockIndex !== null ? nextSectionBlockIndex : Infinity;
|
|
931
|
+
|
|
932
|
+
for (const [type, data] of Object.entries(state.blockSummary)) {
|
|
933
|
+
for (const block of data.blocks) {
|
|
934
|
+
if (block.index > startIdx && block.index < endIdx) {
|
|
935
|
+
sectionBlocks.push({ type, ...block });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
sectionBlocks.sort((a, b) => a.index - b.index);
|
|
940
|
+
|
|
941
|
+
const blockList = sectionBlocks
|
|
942
|
+
.map(b => {
|
|
943
|
+
const indent = ' '.repeat((b.depth || 0) + 1);
|
|
944
|
+
let marker = '';
|
|
945
|
+
let info = '';
|
|
946
|
+
if (b.isContainer) {
|
|
947
|
+
marker = '▼ ';
|
|
948
|
+
if (b.columnCount) {
|
|
949
|
+
info = ` [${b.columnCount}カラム]`;
|
|
950
|
+
} else if (b.tabNames && b.tabNames.length > 0) {
|
|
951
|
+
info = ` [${b.tabNames.join(", ")}]`;
|
|
952
|
+
} else if (b.childCount) {
|
|
953
|
+
info = ` [${b.childCount}子]`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return `${indent}${b.index}: ${marker}${b.type}${info} (d:${b.depth || 0})`;
|
|
957
|
+
})
|
|
958
|
+
.join("\n");
|
|
959
|
+
|
|
960
|
+
const nextInfo = nextSectionBlockIndex !== null
|
|
961
|
+
? `\n次のセクション: index ${nextSectionBlockIndex}`
|
|
962
|
+
: "\n(最後のセクション)";
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
content: [{
|
|
966
|
+
type: "text",
|
|
967
|
+
text: `📊 get_article_structure(${paramLabel})\n\n${sectionHeading.text} セクション (index:${sectionHeading.index}):\n\n${blockList}${nextInfo}`,
|
|
968
|
+
}],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ========================================
|
|
973
|
+
// 特定レベルの見出しのみ
|
|
974
|
+
// ========================================
|
|
975
|
+
if (headingLevel) {
|
|
976
|
+
const filteredHeadings = state.headings.filter(h => h.level === headingLevel);
|
|
977
|
+
if (filteredHeadings.length === 0) {
|
|
978
|
+
return {
|
|
979
|
+
content: [{
|
|
980
|
+
type: "text",
|
|
981
|
+
text: `📊 get_article_structure(${paramLabel})\n\n❌ H${headingLevel}見出しは記事内に存在しません。`,
|
|
982
|
+
}],
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const headingList = filteredHeadings
|
|
986
|
+
.map(h => ` ${"#".repeat(h.level)} ${h.text} (index:${h.index})`)
|
|
987
|
+
.join("\n");
|
|
988
|
+
return {
|
|
989
|
+
content: [{
|
|
990
|
+
type: "text",
|
|
991
|
+
text: `📊 get_article_structure(${paramLabel})\n\nH${headingLevel}見出し一覧 (${filteredHeadings.length}個):\n\n${headingList}`,
|
|
992
|
+
}],
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ========================================
|
|
997
|
+
// デフォルト - 見出し構造のみ(軽量)
|
|
998
|
+
// ========================================
|
|
999
|
+
let output = `📊 get_article_structure(${paramLabel})\n\n見出し構造:\n\n`;
|
|
1000
|
+
|
|
1001
|
+
for (const heading of state.headings) {
|
|
1002
|
+
const indent = heading.level === 2 ? "" : " ";
|
|
1003
|
+
const prefix = "#".repeat(heading.level);
|
|
1004
|
+
output += `${indent}${prefix} ${heading.text} (H${heading.level}, index:${heading.index})\n`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
content: [{ type: "text", text: output }],
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
case "select_block": {
|
|
1013
|
+
const { mode, message, client } = await resolveMode(args);
|
|
1014
|
+
if (mode === 'error') {
|
|
1015
|
+
return errorResponse(name, message, args?.site);
|
|
1016
|
+
}
|
|
1017
|
+
if (mode !== 'editor') {
|
|
1018
|
+
return { content: [{ type: "text", text: `❌ select_block はエディタ接続時のみ使用可能です。` }], isError: true };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const { index, blockType, typeIndex, contains, startIndex, endIndex, headingLevel, headingContains } = (args || {});
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
await client.selectBlock({
|
|
1025
|
+
index, blockType, typeIndex, contains,
|
|
1026
|
+
startIndex, endIndex, headingLevel, headingContains
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
return {
|
|
1030
|
+
content: [{
|
|
1031
|
+
type: "text",
|
|
1032
|
+
text: `✅ ブロック選択リクエスト送信完了\n\n` +
|
|
1033
|
+
`次のステップ:\n` +
|
|
1034
|
+
`- get_selection で選択状態を確認\n` +
|
|
1035
|
+
`- update_blocks で編集`,
|
|
1036
|
+
}],
|
|
1037
|
+
};
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return {
|
|
1040
|
+
content: [{ type: "text", text: `❌ 選択失敗: ${error.message}` }],
|
|
1041
|
+
isError: true,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
case "delete_block": {
|
|
1047
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1048
|
+
if (mode === 'error') {
|
|
1049
|
+
return errorResponse(name, message, args?.site);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const { index, count, indices } = args;
|
|
1053
|
+
|
|
1054
|
+
// indices と index/count の排他バリデーション
|
|
1055
|
+
if (indices !== undefined && (index !== undefined || count !== undefined)) {
|
|
1056
|
+
return { content: [{ type: "text", text: "❌ indices と index/count は同時に指定できません。" }], isError: true };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// indices モード(非連続一括削除)
|
|
1060
|
+
if (indices !== undefined) {
|
|
1061
|
+
if (!Array.isArray(indices) || indices.length === 0) {
|
|
1062
|
+
return { content: [{ type: "text", text: "❌ indices は1つ以上の数値を含む配列で指定してください。" }], isError: true };
|
|
1063
|
+
}
|
|
1064
|
+
// 重複排除
|
|
1065
|
+
const uniqueIndices = [...new Set(indices)].filter(i => Number.isInteger(i));
|
|
1066
|
+
if (uniqueIndices.length === 0) {
|
|
1067
|
+
return { content: [{ type: "text", text: "❌ indices に有効な整数が含まれていません。" }], isError: true };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (mode === 'headless') {
|
|
1071
|
+
const result = await client.headlessDeleteMultiple(postId, uniqueIndices);
|
|
1072
|
+
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1073
|
+
return { content: [{ type: "text", text: `✅ ${result.count}ブロック削除\n\n${details}` }] };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const result = await client.sendEditorCommand("delete_block", { indices: uniqueIndices });
|
|
1077
|
+
if (!result)
|
|
1078
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1079
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1080
|
+
if (!result.success)
|
|
1081
|
+
return errorResponse(name, result.error, args?.site);
|
|
1082
|
+
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1083
|
+
return { content: [{ type: "text", text: `✅ ${result.count}ブロック削除\n\n${details}` }] };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// 既存モード(index + count)
|
|
1087
|
+
if (mode === 'headless') {
|
|
1088
|
+
if (index === undefined) {
|
|
1089
|
+
return { content: [{ type: "text", text: "❌ Headless モードでは index の指定が必須です。" }], isError: true };
|
|
1090
|
+
}
|
|
1091
|
+
const result = await client.headlessDelete(postId, index, count || 1);
|
|
1092
|
+
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1093
|
+
return { content: [{ type: "text", text: `✅ ${result.count}ブロック削除\n\n${details}` }] };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const result = await client.sendEditorCommand("delete_block", { index, count });
|
|
1097
|
+
if (!result)
|
|
1098
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1099
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1100
|
+
if (!result.success)
|
|
1101
|
+
return errorResponse(name, result.error, args?.site);
|
|
1102
|
+
const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
|
|
1103
|
+
return { content: [{ type: "text", text: `✅ ${result.count}ブロック削除\n\n${details}` }] };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
case "move_block": {
|
|
1107
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1108
|
+
if (mode === 'error') {
|
|
1109
|
+
return errorResponse(name, message, args?.site);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const { from, to, fromFlat, toFlat } = args;
|
|
1113
|
+
|
|
1114
|
+
// 排他バリデーション
|
|
1115
|
+
const hasTopLevel = from !== undefined || to !== undefined;
|
|
1116
|
+
const hasFlat = fromFlat !== undefined || toFlat !== undefined;
|
|
1117
|
+
if (hasTopLevel && hasFlat) {
|
|
1118
|
+
return { content: [{ type: "text", text: "❌ from/to と fromFlat/toFlat は同時に指定できません。" }], isError: true };
|
|
1119
|
+
}
|
|
1120
|
+
if (!hasTopLevel && !hasFlat) {
|
|
1121
|
+
return { content: [{ type: "text", text: "❌ from/to または fromFlat/toFlat を指定してください。" }], isError: true };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// fromFlat/toFlat モード(フラットインデックス移動)
|
|
1125
|
+
if (hasFlat) {
|
|
1126
|
+
if (fromFlat === undefined || toFlat === undefined) {
|
|
1127
|
+
return { content: [{ type: "text", text: "❌ fromFlat と toFlat の両方を指定してください。" }], isError: true };
|
|
1128
|
+
}
|
|
1129
|
+
if (fromFlat === toFlat) {
|
|
1130
|
+
return { content: [{ type: "text", text: "❌ fromFlat と toFlat が同じ位置です。" }], isError: true };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (mode === 'headless') {
|
|
1134
|
+
const result = await client.headlessMoveFlat(postId, fromFlat, toFlat);
|
|
1135
|
+
return { content: [{ type: "text", text: `✅ ブロック移動: flat[${fromFlat}] → flat[${toFlat}] (${result.moved.type})` }] };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const result = await client.sendEditorCommand("move_block", { fromFlat, toFlat });
|
|
1139
|
+
if (!result)
|
|
1140
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1141
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1142
|
+
if (!result.success)
|
|
1143
|
+
return errorResponse(name, result.error, args?.site);
|
|
1144
|
+
return { content: [{ type: "text", text: `✅ ブロック移動: flat[${fromFlat}] → flat[${toFlat}] (${result.moved.type})` }] };
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// 既存モード(from/to トップレベル)
|
|
1148
|
+
if (from === undefined || to === undefined) {
|
|
1149
|
+
return { content: [{ type: "text", text: "❌ from と to の両方を指定してください" }], isError: true };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (mode === 'headless') {
|
|
1153
|
+
const result = await client.headlessMove(postId, from, to);
|
|
1154
|
+
return { content: [{ type: "text", text: `✅ ブロック移動: [${from}] → [${to}] (${result.moved.type})` }] };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const result = await client.sendEditorCommand("move_block", { from, to });
|
|
1158
|
+
if (!result)
|
|
1159
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1160
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1161
|
+
if (!result.success)
|
|
1162
|
+
return errorResponse(name, result.error, args?.site);
|
|
1163
|
+
if (result.moved?.noop)
|
|
1164
|
+
return { content: [{ type: "text", text: `✅ 移動不要(同じ位置)` }] };
|
|
1165
|
+
return { content: [{ type: "text", text: `✅ ブロック移動: [${from}] → [${to}] (${result.moved.type})` }] };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
case "undo": {
|
|
1169
|
+
const { mode, message, client } = await resolveMode(args);
|
|
1170
|
+
if (mode === 'error') {
|
|
1171
|
+
return errorResponse(name, message, args?.site);
|
|
1172
|
+
}
|
|
1173
|
+
if (mode !== 'editor') {
|
|
1174
|
+
return { content: [{ type: "text", text: `❌ undo はエディタ接続時のみ使用可能です。` }], isError: true };
|
|
1175
|
+
}
|
|
1176
|
+
const result = await client.sendEditorCommand("undo", { steps: args?.steps });
|
|
1177
|
+
if (!result)
|
|
1178
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1179
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1180
|
+
if (!result.success)
|
|
1181
|
+
return errorResponse(name, result.error, args?.site);
|
|
1182
|
+
return { content: [{ type: "text", text: `✅ ${result.done}回取り消し` }] };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
case "redo": {
|
|
1186
|
+
const { mode, message, client } = await resolveMode(args);
|
|
1187
|
+
if (mode === 'error') {
|
|
1188
|
+
return errorResponse(name, message, args?.site);
|
|
1189
|
+
}
|
|
1190
|
+
if (mode !== 'editor') {
|
|
1191
|
+
return { content: [{ type: "text", text: `❌ redo はエディタ接続時のみ使用可能です。` }], isError: true };
|
|
1192
|
+
}
|
|
1193
|
+
const result = await client.sendEditorCommand("redo", { steps: args?.steps });
|
|
1194
|
+
if (!result)
|
|
1195
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1196
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1197
|
+
if (!result.success)
|
|
1198
|
+
return errorResponse(name, result.error, args?.site);
|
|
1199
|
+
return { content: [{ type: "text", text: `✅ ${result.done}回やり直し` }] };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
case "duplicate_block": {
|
|
1203
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1204
|
+
if (mode === 'error') {
|
|
1205
|
+
return errorResponse(name, message, args?.site);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const { index } = args;
|
|
1209
|
+
|
|
1210
|
+
if (mode === 'headless') {
|
|
1211
|
+
if (index === undefined) {
|
|
1212
|
+
return { content: [{ type: "text", text: "❌ Headless モードでは index の指定が必須です。" }], isError: true };
|
|
1213
|
+
}
|
|
1214
|
+
const result = await client.headlessDuplicate(postId, index);
|
|
1215
|
+
return { content: [{ type: "text", text: `✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})` }] };
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const result = await client.sendEditorCommand("duplicate_block", { index });
|
|
1219
|
+
if (!result)
|
|
1220
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1221
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1222
|
+
if (!result.success)
|
|
1223
|
+
return errorResponse(name, result.error, args?.site);
|
|
1224
|
+
return { content: [{ type: "text", text: `✅ ブロック複製完了` }] };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
case "save_post": {
|
|
1228
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1229
|
+
if (mode === 'error') {
|
|
1230
|
+
return errorResponse(name, message, args?.site);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (mode === 'headless') {
|
|
1234
|
+
// Headless モードでは即 DB 書き込みのため save は不要
|
|
1235
|
+
return { content: [{ type: "text", text: `✅ Headless モードでは変更は即時保存されています。` }] };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const result = await client.sendEditorCommand("save_post", {});
|
|
1239
|
+
if (!result)
|
|
1240
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1241
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1242
|
+
if (!result.success)
|
|
1243
|
+
return errorResponse(name, result.error, args?.site);
|
|
1244
|
+
return { content: [{ type: "text", text: `✅ 保存完了` }] };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
case "get_post_meta": {
|
|
1248
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1249
|
+
if (mode === 'error') {
|
|
1250
|
+
return errorResponse(name, message, args?.site);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
let m;
|
|
1254
|
+
if (mode === 'headless') {
|
|
1255
|
+
m = await client.headlessGetMeta(postId);
|
|
1256
|
+
} else {
|
|
1257
|
+
const result = await client.sendEditorCommand("get_post_meta", {});
|
|
1258
|
+
if (!result)
|
|
1259
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1260
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1261
|
+
if (!result.success)
|
|
1262
|
+
return errorResponse(name, result.error, args?.site);
|
|
1263
|
+
m = result.meta;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const statusInfo = mode === 'editor'
|
|
1267
|
+
? `モード: Editor (接続中)`
|
|
1268
|
+
: `モード: Headless (postId: ${postId})`;
|
|
1269
|
+
const text = [
|
|
1270
|
+
`📄 投稿メタ情報`,
|
|
1271
|
+
``,
|
|
1272
|
+
statusInfo,
|
|
1273
|
+
`ID: ${m.id}`,
|
|
1274
|
+
`タイトル: ${m.title}`,
|
|
1275
|
+
`ステータス: ${m.status}`,
|
|
1276
|
+
`スラッグ: ${m.slug}`,
|
|
1277
|
+
`URL: ${m.link}`,
|
|
1278
|
+
`カテゴリ: ${JSON.stringify(m.categories)}`,
|
|
1279
|
+
`タグ: ${JSON.stringify(m.tags)}`,
|
|
1280
|
+
`アイキャッチ: ${m.featuredImage || 'なし'}`,
|
|
1281
|
+
`抜粋: ${m.excerpt || 'なし'}`,
|
|
1282
|
+
`更新日: ${m.modified}`,
|
|
1283
|
+
].join('\n');
|
|
1284
|
+
return { content: [{ type: "text", text }] };
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
case "update_post_meta": {
|
|
1288
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1289
|
+
if (mode === 'error') {
|
|
1290
|
+
return errorResponse(name, message, args?.site);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const updateData = {};
|
|
1294
|
+
if (args.title !== undefined) updateData.title = args.title;
|
|
1295
|
+
if (args.status !== undefined) updateData.status = args.status;
|
|
1296
|
+
if (args.slug !== undefined) updateData.slug = args.slug;
|
|
1297
|
+
if (args.categories !== undefined) updateData.categories = args.categories;
|
|
1298
|
+
if (args.tags !== undefined) updateData.tags = args.tags;
|
|
1299
|
+
if (args.excerpt !== undefined) updateData.excerpt = args.excerpt;
|
|
1300
|
+
if (args.featured_media !== undefined) updateData.featured_media = args.featured_media;
|
|
1301
|
+
|
|
1302
|
+
if (Object.keys(updateData).length === 0) {
|
|
1303
|
+
return { content: [{ type: "text", text: "❌ 更新するフィールドを1つ以上指定してください" }], isError: true };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
await client.updatePostMeta(postId, updateData);
|
|
1307
|
+
|
|
1308
|
+
const updated = Object.keys(updateData).map(k => ` ${k}: ${JSON.stringify(updateData[k])}`).join('\n');
|
|
1309
|
+
let updateText = `✅ メタ情報更新完了 (postId: ${postId})\n\n${updated}`;
|
|
1310
|
+
|
|
1311
|
+
if (mode === 'editor') {
|
|
1312
|
+
updateText += `\n\n⚠️ エディタで同じ記事を開いています。エディタ側で未保存の変更がある場合、エディタの「保存」で上書きされる可能性があります。エディタをリロードしてください。`;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return { content: [{ type: "text", text: updateText }] };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
case "list_posts": {
|
|
1319
|
+
let client;
|
|
1320
|
+
try {
|
|
1321
|
+
client = registry.get(args?.site);
|
|
1322
|
+
} catch (e) {
|
|
1323
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1324
|
+
}
|
|
1325
|
+
const { data: posts, total, totalPages } = await client.listPosts(args || {});
|
|
1326
|
+
|
|
1327
|
+
if (!posts || posts.length === 0) {
|
|
1328
|
+
return { content: [{ type: "text", text: "📋 該当する投稿はありません。" }] };
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const currentPage = args?.page || 1;
|
|
1332
|
+
const list = posts.map(p => {
|
|
1333
|
+
return ` [${p.id}] ${p.title?.rendered || '(無題)'}\n` +
|
|
1334
|
+
` ステータス: ${p.status} | 更新: ${p.modified?.split('T')[0] || '-'}\n` +
|
|
1335
|
+
` URL: ${p.link || '-'}`;
|
|
1336
|
+
}).join('\n\n');
|
|
1337
|
+
|
|
1338
|
+
let listText = `📋 投稿一覧 (${posts.length}件 / 全${total}件)\n\n${list}`;
|
|
1339
|
+
if (totalPages > 1) {
|
|
1340
|
+
listText += `\n\n📄 ページ: ${currentPage} / ${totalPages}`;
|
|
1341
|
+
if (currentPage < totalPages) {
|
|
1342
|
+
listText += ` (次: page=${currentPage + 1})`;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return { content: [{ type: "text", text: listText }] };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
case "list_taxonomies": {
|
|
1350
|
+
let client;
|
|
1351
|
+
try {
|
|
1352
|
+
client = registry.get(args?.site);
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const taxType = args?.type;
|
|
1358
|
+
if (!taxType || !['category', 'tag'].includes(taxType)) {
|
|
1359
|
+
return { content: [{ type: "text", text: "❌ type は 'category' または 'tag' を指定してください" }], isError: true };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const { data: terms, total, totalPages } = await client.listTaxonomies(taxType, args || {});
|
|
1363
|
+
|
|
1364
|
+
if (!terms || terms.length === 0) {
|
|
1365
|
+
return { content: [{ type: "text", text: `📋 該当する${taxType === 'category' ? 'カテゴリ' : 'タグ'}はありません。` }] };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const label = taxType === 'category' ? 'カテゴリ' : 'タグ';
|
|
1369
|
+
const currentPage = args?.page || 1;
|
|
1370
|
+
const list = terms.map(t => {
|
|
1371
|
+
return ` [${t.id}] ${t.name}` +
|
|
1372
|
+
(t.count !== undefined ? ` (${t.count}件)` : '') +
|
|
1373
|
+
(t.parent ? ` 親: ${t.parent}` : '');
|
|
1374
|
+
}).join('\n');
|
|
1375
|
+
|
|
1376
|
+
let text = `📋 ${label}一覧 (${terms.length}件 / 全${total}件)\n\n${list}`;
|
|
1377
|
+
if (totalPages > 1) {
|
|
1378
|
+
text += `\n\n📄 ページ: ${currentPage} / ${totalPages}`;
|
|
1379
|
+
if (currentPage < totalPages) {
|
|
1380
|
+
text += ` (次: page=${currentPage + 1})`;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return { content: [{ type: "text", text }] };
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
case "insert_block": {
|
|
1388
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1389
|
+
if (mode === 'error') {
|
|
1390
|
+
return errorResponse(name, message, args?.site);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const { blockType, content, index, blocks, rawHTML } = args;
|
|
1394
|
+
|
|
1395
|
+
// 排他バリデーション
|
|
1396
|
+
const hasLegacy = blockType !== undefined || content !== undefined;
|
|
1397
|
+
const hasBlocks = blocks !== undefined;
|
|
1398
|
+
const hasRawHTML = rawHTML !== undefined;
|
|
1399
|
+
const modeCount = [hasLegacy, hasBlocks, hasRawHTML].filter(Boolean).length;
|
|
1400
|
+
if (modeCount === 0) {
|
|
1401
|
+
return { content: [{ type: "text", text: "❌ blockType+content、blocks、rawHTML のいずれかを指定してください。" }], isError: true };
|
|
1402
|
+
}
|
|
1403
|
+
if (modeCount > 1) {
|
|
1404
|
+
return { content: [{ type: "text", text: "❌ blockType+content、blocks、rawHTML は同時に指定できません。" }], isError: true };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// 複数挿入モード (blocks / rawHTML)
|
|
1408
|
+
if (hasBlocks || hasRawHTML) {
|
|
1409
|
+
if (hasBlocks && (!Array.isArray(blocks) || blocks.length === 0)) {
|
|
1410
|
+
return { content: [{ type: "text", text: "❌ blocks は1つ以上の要素を含む配列で指定してください。" }], isError: true };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (mode === 'headless') {
|
|
1414
|
+
const params = { index };
|
|
1415
|
+
if (hasBlocks) params.blocks = blocks;
|
|
1416
|
+
if (hasRawHTML) params.rawHTML = rawHTML;
|
|
1417
|
+
const result = await client.headlessInsert(postId, params);
|
|
1418
|
+
const count = result.insertedIndices ? result.insertedIndices.length : result.count || 1;
|
|
1419
|
+
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at ${index ?? 'end'}` }] };
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const cmdParams = { index };
|
|
1423
|
+
if (hasBlocks) cmdParams.blocks = blocks;
|
|
1424
|
+
if (hasRawHTML) cmdParams.rawHTML = rawHTML;
|
|
1425
|
+
const result = await client.sendEditorCommand("insert_block", cmdParams);
|
|
1426
|
+
if (!result)
|
|
1427
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1428
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1429
|
+
if (!result.success)
|
|
1430
|
+
return errorResponse(name, result.error, args?.site);
|
|
1431
|
+
const count = result.insertedCount || result.inserted?.length || 1;
|
|
1432
|
+
return { content: [{ type: "text", text: `✅ ${count}ブロック挿入 at ${index ?? 'end'}` }] };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// 既存モード(単一 blockType + content)
|
|
1436
|
+
if (!blockType || content === undefined) {
|
|
1437
|
+
return { content: [{ type: "text", text: "❌ blockType と content は必須です" }], isError: true };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (mode === 'headless') {
|
|
1441
|
+
const result = await client.headlessInsert(postId, { blockType, content, index });
|
|
1442
|
+
return { content: [{ type: "text", text: `✅ ブロック挿入: ${blockType} at ${result.newIndex}` }] };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const result = await client.sendEditorCommand("insert_block", { blockType, content, index });
|
|
1446
|
+
if (!result)
|
|
1447
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1448
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1449
|
+
if (!result.success)
|
|
1450
|
+
return errorResponse(name, result.error, args?.site);
|
|
1451
|
+
return { content: [{ type: "text", text: `✅ ブロック挿入: ${result.inserted.type} at ${result.inserted.index}` }] };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
case "get_block_html": {
|
|
1455
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1456
|
+
if (mode === 'error') {
|
|
1457
|
+
return errorResponse(name, message, args?.site);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex, headingLevel, headingContains } = (args || {});
|
|
1461
|
+
|
|
1462
|
+
if (mode === 'headless' && target === 'selected') {
|
|
1463
|
+
return {
|
|
1464
|
+
content: [{ type: "text", text: "❌ target:'selected' はエディタ接続時のみ使用可能です。index を指定してください。" }],
|
|
1465
|
+
isError: true,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const hasTarget = target || index !== undefined || indices ||
|
|
1470
|
+
startIndex !== undefined || section || blockType ||
|
|
1471
|
+
(headingLevel && headingContains);
|
|
1472
|
+
if (!hasTarget) {
|
|
1473
|
+
return {
|
|
1474
|
+
content: [{ type: "text", text: "❌ ターゲットが指定されていません。\ntarget, index, indices, section, blockType, contains 等のいずれかを指定してください。" }],
|
|
1475
|
+
isError: true,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
let blocks;
|
|
1480
|
+
if (mode === 'headless') {
|
|
1481
|
+
const result = await client.headlessGetBlockHtml(postId, {
|
|
1482
|
+
index, indices, startIndex, endIndex,
|
|
1483
|
+
section, blockType, typeIndex,
|
|
1484
|
+
headingLevel, headingContains,
|
|
1485
|
+
});
|
|
1486
|
+
blocks = result.blocks || [];
|
|
1487
|
+
} else {
|
|
1488
|
+
const result = await client.sendEditorCommand("get_block_html", { target, index, indices, startIndex, endIndex, section, blockType, typeIndex, headingLevel, headingContains }, 10000);
|
|
1489
|
+
if (!result) {
|
|
1490
|
+
return errorResponse(name, 'タイムアウト: WordPress側からの応答がありません', args?.site);
|
|
1491
|
+
}
|
|
1492
|
+
if (result.error) {
|
|
1493
|
+
return errorResponse(name, result.error, args?.site);
|
|
1494
|
+
}
|
|
1495
|
+
blocks = result.blocks || [];
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
let text = `📦 ブロックHTML取得 (${blocks.length}件)\n`;
|
|
1499
|
+
for (const b of blocks) {
|
|
1500
|
+
text += `\n[${b.index}] ${b.type}\n${b.html}\n`;
|
|
1501
|
+
}
|
|
1502
|
+
return { content: [{ type: "text", text }] };
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
case "get_selection": {
|
|
1506
|
+
const { mode, message, client } = await resolveMode(args);
|
|
1507
|
+
if (mode === 'error') {
|
|
1508
|
+
return errorResponse(name, message, args?.site);
|
|
1509
|
+
}
|
|
1510
|
+
if (mode !== 'editor') {
|
|
1511
|
+
return { content: [{ type: "text", text: `❌ get_selection はエディタ接続時のみ使用可能です。` }], isError: true };
|
|
1512
|
+
}
|
|
1513
|
+
const state = await client.getEditorState();
|
|
1514
|
+
const sel = state.selectedBlock;
|
|
1515
|
+
|
|
1516
|
+
if (!sel || (!sel.blockId && !sel.blockIds)) {
|
|
1517
|
+
return {
|
|
1518
|
+
content: [{ type: "text", text: "ブロックが選択されていません。" }],
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
// 複数選択
|
|
1522
|
+
if (sel.isMultiSelect && sel.blockIds) {
|
|
1523
|
+
let text = `選択中: ${sel.blockIds.length}ブロック\n` +
|
|
1524
|
+
`タイプ: ${sel.blockTypes?.join(", ")}\n` +
|
|
1525
|
+
`位置: ${sel.blockIndices?.join(", ")}`;
|
|
1526
|
+
return { content: [{ type: "text", text }] };
|
|
1527
|
+
}
|
|
1528
|
+
// 単一選択
|
|
1529
|
+
let text = `選択中: ${sel.blockType}\n` +
|
|
1530
|
+
`位置: index ${sel.blockIndex}`;
|
|
1531
|
+
if (sel.textSelection?.text) {
|
|
1532
|
+
text += `\n\nカーソル選択テキスト: "${sel.textSelection.text}"`;
|
|
1533
|
+
if (sel.textSelection.context) {
|
|
1534
|
+
text += `\n 前: "...${sel.textSelection.context.before}"`;
|
|
1535
|
+
text += `\n 後: "${sel.textSelection.context.after}..."`;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return { content: [{ type: "text", text }] };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
case "update_blocks": {
|
|
1542
|
+
const { mode, postId: modePostId, message, client } = await resolveMode(args);
|
|
1543
|
+
if (mode === 'error') {
|
|
1544
|
+
return errorResponse(name, message, args?.site);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const { target, index, indices, startIndex, endIndex, section, blockType, typeIndex, contains, headingLevel, headingContains, replacements, newHTML, attributeUpdates, insertOnly, insertPosition, dryRun } = (args || {});
|
|
1548
|
+
|
|
1549
|
+
// Headless モードで target: "selected" はエラー
|
|
1550
|
+
if (mode === 'headless' && target === 'selected') {
|
|
1551
|
+
return {
|
|
1552
|
+
content: [{ type: "text", text: "❌ target:'selected' はエディタ接続時のみ使用可能です。index を指定してください。" }],
|
|
1553
|
+
isError: true,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const hasTarget = target || index !== undefined || indices ||
|
|
1558
|
+
startIndex !== undefined || section || blockType || contains ||
|
|
1559
|
+
(headingLevel && headingContains);
|
|
1560
|
+
if (!hasTarget) {
|
|
1561
|
+
return {
|
|
1562
|
+
content: [{ type: "text", text: "❌ ターゲットが指定されていません。\ntarget, index, indices, section, blockType, contains 等のいずれかを指定してください。" }],
|
|
1563
|
+
isError: true,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const hasUpdate = (replacements && replacements.length > 0) || newHTML || attributeUpdates;
|
|
1568
|
+
if (!hasUpdate) {
|
|
1569
|
+
return {
|
|
1570
|
+
content: [{ type: "text", text: "❌ 変更内容が指定されていません。\nreplacements, newHTML, attributeUpdates のいずれかを指定してください。" }],
|
|
1571
|
+
isError: true,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// ========================================
|
|
1576
|
+
// Headless モード
|
|
1577
|
+
// ========================================
|
|
1578
|
+
if (mode === 'headless') {
|
|
1579
|
+
const result = await client.headlessUpdate(modePostId, {
|
|
1580
|
+
index, indices, startIndex, endIndex,
|
|
1581
|
+
section, blockType, typeIndex, contains,
|
|
1582
|
+
headingLevel, headingContains,
|
|
1583
|
+
replacements, newHTML, attributeUpdates,
|
|
1584
|
+
insertOnly: insertOnly || false,
|
|
1585
|
+
insertPosition,
|
|
1586
|
+
dryRun: dryRun || false,
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
if (result.dryRun && result.results) {
|
|
1590
|
+
const previewText = result.results
|
|
1591
|
+
.map(r => {
|
|
1592
|
+
const label = r.index !== undefined ? `[${r.index}]` :
|
|
1593
|
+
r.indices ? `[${r.indices.join(', ')}]` : '[?]';
|
|
1594
|
+
return ` ${label}: ${r.preview ? 'プレビューあり' : 'N/A'}`;
|
|
1595
|
+
})
|
|
1596
|
+
.join('\n');
|
|
1597
|
+
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}` }] };
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (result.results) {
|
|
1601
|
+
const successCount = result.results.filter(r => r.success).length;
|
|
1602
|
+
const failCount = result.results.filter(r => !r.success).length;
|
|
1603
|
+
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}` }] };
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
return { content: [{ type: "text", text: `✅ 更新完了` }] };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ========================================
|
|
1610
|
+
// Editor モード(現行ロジック)
|
|
1611
|
+
// ========================================
|
|
1612
|
+
|
|
1613
|
+
// target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
|
|
1614
|
+
if (target === "selected") {
|
|
1615
|
+
const state = await client.getEditorState();
|
|
1616
|
+
const sel = state.selectedBlock;
|
|
1617
|
+
|
|
1618
|
+
if (!sel || (!sel.blockId && !sel.blockIds)) {
|
|
1619
|
+
return {
|
|
1620
|
+
content: [{ type: "text", text: "❌ ブロックが選択されていません。" }],
|
|
1621
|
+
isError: true,
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const blockIndices = sel.isMultiSelect ? sel.blockIndices : [sel.blockIndex];
|
|
1626
|
+
|
|
1627
|
+
// replacementsの場合、MCP側でカーソル選択ピンポイント差分を適用
|
|
1628
|
+
if (replacements && replacements.length > 0) {
|
|
1629
|
+
if (!sel.blockHTML) {
|
|
1630
|
+
return {
|
|
1631
|
+
content: [{ type: "text", text: "❌ 選択中ブロックのHTML情報がありません。" }],
|
|
1632
|
+
isError: true,
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
let finalHTML = sel.blockHTML;
|
|
1636
|
+
const hasTextSelection = sel.textSelection && sel.textSelection.text;
|
|
1637
|
+
for (const replacement of replacements) {
|
|
1638
|
+
if (!replacement.old || !replacement.new) continue;
|
|
1639
|
+
if (hasTextSelection && sel.textSelection) {
|
|
1640
|
+
const { text: selText, context } = sel.textSelection;
|
|
1641
|
+
const searchPattern = context.before + selText + context.after;
|
|
1642
|
+
if (finalHTML.includes(searchPattern)) {
|
|
1643
|
+
const replacedPattern = context.before + replacement.new + context.after;
|
|
1644
|
+
finalHTML = finalHTML.replace(searchPattern, replacedPattern);
|
|
1645
|
+
} else {
|
|
1646
|
+
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1647
|
+
}
|
|
1648
|
+
} else {
|
|
1649
|
+
finalHTML = finalHTML.replace(new RegExp(escapeRegExp(replacement.old), 'g'), replacement.new);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1653
|
+
mode: "html_replace_by_index",
|
|
1654
|
+
blockIndices,
|
|
1655
|
+
newHTML: finalHTML,
|
|
1656
|
+
insertOnly: insertOnly || false,
|
|
1657
|
+
insertPosition,
|
|
1658
|
+
}, 10000);
|
|
1659
|
+
if (!result)
|
|
1660
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1661
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1662
|
+
if (!result.success)
|
|
1663
|
+
return errorResponse(name, result.error, args?.site);
|
|
1664
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}` }] };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// newHTMLの場合
|
|
1668
|
+
if (newHTML) {
|
|
1669
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1670
|
+
mode: "html_replace_by_index",
|
|
1671
|
+
blockIndices,
|
|
1672
|
+
newHTML,
|
|
1673
|
+
insertOnly: insertOnly || false,
|
|
1674
|
+
insertPosition,
|
|
1675
|
+
}, 10000);
|
|
1676
|
+
if (!result)
|
|
1677
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1678
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1679
|
+
if (!result.success)
|
|
1680
|
+
return errorResponse(name, result.error, args?.site);
|
|
1681
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了` }] };
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// attributeUpdatesの場合
|
|
1685
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1686
|
+
mode: "attribute_update_by_index",
|
|
1687
|
+
blockIndices,
|
|
1688
|
+
attributeUpdates,
|
|
1689
|
+
dryRun: dryRun || false,
|
|
1690
|
+
}, 10000);
|
|
1691
|
+
if (!result)
|
|
1692
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1693
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1694
|
+
if (!result.success)
|
|
1695
|
+
return errorResponse(name, result.error, args?.site);
|
|
1696
|
+
return { content: [{ type: "text", text: `✅ 属性更新完了` }] };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// その他のターゲット → 全てWP側で解決&実行
|
|
1700
|
+
const maxWait = (section || blockType || contains) ? 15000 : 10000;
|
|
1701
|
+
const result = await client.sendEditorCommand("update_blocks", {
|
|
1702
|
+
index, indices, startIndex, endIndex,
|
|
1703
|
+
section, blockType, typeIndex, contains,
|
|
1704
|
+
headingLevel, headingContains,
|
|
1705
|
+
replacements, newHTML, attributeUpdates,
|
|
1706
|
+
insertOnly: insertOnly || false,
|
|
1707
|
+
insertPosition,
|
|
1708
|
+
dryRun: dryRun || false,
|
|
1709
|
+
}, maxWait);
|
|
1710
|
+
if (!result)
|
|
1711
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1712
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1713
|
+
if (!result.success)
|
|
1714
|
+
return errorResponse(name, result.error, args?.site);
|
|
1715
|
+
|
|
1716
|
+
// 結果フォーマット
|
|
1717
|
+
if (dryRun && result.results) {
|
|
1718
|
+
const previewText = result.results
|
|
1719
|
+
.filter(r => r.preview)
|
|
1720
|
+
.map(r => {
|
|
1721
|
+
if (Array.isArray(r.preview)) {
|
|
1722
|
+
return ` [${r.index}]\n${r.preview.map(p => ` "${p.old}" → "${p.new}" (${p.count}箇所)`).join('\n')}`;
|
|
1723
|
+
} else if (r.preview?.willUpdate) {
|
|
1724
|
+
return ` [${r.index}]: 属性更新予定 ${JSON.stringify(r.preview.willUpdate)}`;
|
|
1725
|
+
} else if (r.preview?.skipped) {
|
|
1726
|
+
return ` [${r.index}]: スキップ (${r.preview.skipped})`;
|
|
1727
|
+
}
|
|
1728
|
+
return ` [${r.index}]: プレビュー不可`;
|
|
1729
|
+
})
|
|
1730
|
+
.join('\n');
|
|
1731
|
+
return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}` }] };
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (result.results) {
|
|
1735
|
+
const successCount = result.results.filter(r => r.success && !r.skipped).length;
|
|
1736
|
+
const skipCount = result.results.filter(r => r.skipped).length;
|
|
1737
|
+
const failCount = result.results.filter(r => !r.success).length;
|
|
1738
|
+
const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
|
|
1739
|
+
return { content: [{ type: "text", text: `✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}` }] };
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return { content: [{ type: "text", text: `✅ ${result.mode === "insert" ? "挿入" : "更新"}完了` }] };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
case "table_operations": {
|
|
1746
|
+
const { mode, postId, message, client } = await resolveMode(args);
|
|
1747
|
+
if (mode === 'error') {
|
|
1748
|
+
return errorResponse(name, message, args?.site);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const { index, action } = (args || {});
|
|
1752
|
+
if (index === undefined) {
|
|
1753
|
+
return { content: [{ type: "text", text: "❌ index は必須です" }], isError: true };
|
|
1754
|
+
}
|
|
1755
|
+
if (!action) {
|
|
1756
|
+
return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const tableParams = {
|
|
1760
|
+
index, action,
|
|
1761
|
+
row: args.row, col: args.col,
|
|
1762
|
+
content: args.content, position: args.position,
|
|
1763
|
+
cells: args.cells, style: args.style,
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// get_structure の結果をフォーマットするヘルパー
|
|
1767
|
+
const formatStructure = (s) => {
|
|
1768
|
+
let text = `📊 テーブル構造 (index: ${index})\n`;
|
|
1769
|
+
text += `行数: ${s.totalRows}, 列数: ${s.totalCols}\n`;
|
|
1770
|
+
text += `ヘッダー: ${s.hasHeader ? 'あり' : 'なし'}, フッター: ${s.hasFooter ? 'あり' : 'なし'}\n`;
|
|
1771
|
+
for (const section of (s.sections || [])) {
|
|
1772
|
+
text += `\n[${section.type}]\n`;
|
|
1773
|
+
for (const row of section.rows) {
|
|
1774
|
+
const cellTexts = row.cells.map((c, ci) => {
|
|
1775
|
+
const content = c.content.replace(/<[^>]*>/g, '').trim();
|
|
1776
|
+
return `[${row.globalRow},${ci}] ${content || '(空)'}`;
|
|
1777
|
+
});
|
|
1778
|
+
text += ` 行${row.globalRow}: ${cellTexts.join(' | ')}\n`;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return text;
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
if (mode === 'headless') {
|
|
1785
|
+
const result = await client.headlessTableOperation(postId, tableParams);
|
|
1786
|
+
if (action === 'get_structure') {
|
|
1787
|
+
return { content: [{ type: "text", text: formatStructure(result) }] };
|
|
1788
|
+
}
|
|
1789
|
+
return { content: [{ type: "text", text: `✅ ${action} 完了: ${JSON.stringify(result.detail || result)}` }] };
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Editor モード
|
|
1793
|
+
const result = await client.sendEditorCommand("table_operations", tableParams, 10000);
|
|
1794
|
+
if (!result)
|
|
1795
|
+
{ sendFeedback({ tool: name, category: 'error', content: 'タイムアウト: WordPressエディタが応答していません', site: args?.site || 'default' }); }
|
|
1796
|
+
return { content: [{ type: "text", text: "⏳ タイムアウト: WordPressエディタが応答していません。" }] };
|
|
1797
|
+
if (!result.success)
|
|
1798
|
+
return errorResponse(name, result.error, args?.site);
|
|
1799
|
+
|
|
1800
|
+
if (action === 'get_structure') {
|
|
1801
|
+
return { content: [{ type: "text", text: formatStructure(result.structure) }] };
|
|
1802
|
+
}
|
|
1803
|
+
return { content: [{ type: "text", text: `✅ ${action} 完了: ${JSON.stringify(result.detail || {})}` }] };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
case "list_connections": {
|
|
1807
|
+
const conns = registry.list();
|
|
1808
|
+
const text = conns.map(c => ` ${c.name}: ${c.url} (${c.user})`).join('\n');
|
|
1809
|
+
return { content: [{ type: "text", text: `🔗 接続一覧 (${conns.length}件)\n\n${text}` }] };
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
case "report_feedback": {
|
|
1813
|
+
if (!FEEDBACK_URL) {
|
|
1814
|
+
return { content: [{ type: "text", text: "⚠️ フィードバック送信先が未設定です(FRIDAY_FEEDBACK_URL)" }] };
|
|
1815
|
+
}
|
|
1816
|
+
await sendFeedback({
|
|
1817
|
+
tool: args?.tool || '',
|
|
1818
|
+
category: args?.category || 'feedback',
|
|
1819
|
+
content: args?.content || '',
|
|
1820
|
+
});
|
|
1821
|
+
return { content: [{ type: "text", text: `✅ フィードバックを送信しました` }] };
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
default:
|
|
1825
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1830
|
+
// エラー発生時に自動でフィードバック送信
|
|
1831
|
+
sendFeedback({
|
|
1832
|
+
tool: name,
|
|
1833
|
+
category: 'error',
|
|
1834
|
+
content: errorMessage,
|
|
1835
|
+
site: args?.site || 'default',
|
|
1836
|
+
});
|
|
1837
|
+
return {
|
|
1838
|
+
content: [{
|
|
1839
|
+
type: "text",
|
|
1840
|
+
text: `エラー: ${errorMessage}`,
|
|
1841
|
+
}],
|
|
1842
|
+
isError: true,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
// サーバー起動
|
|
1848
|
+
async function main() {
|
|
1849
|
+
const transport = new StdioServerTransport();
|
|
1850
|
+
await server.connect(transport);
|
|
1851
|
+
const conns = registry.list();
|
|
1852
|
+
console.error("F.R.I.D.A.Y. MCP Server v2.0 running (WP REST API mode, Headless supported)");
|
|
1853
|
+
console.error(` 接続数: ${conns.length}`);
|
|
1854
|
+
for (const c of conns) {
|
|
1855
|
+
console.error(` [${c.name}] ${c.url} (${c.user})`);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
main().catch((error) => {
|
|
1859
|
+
console.error("MCP Server error:", error);
|
|
1860
|
+
process.exit(1);
|
|
1861
|
+
});
|