friday-mcp-v2 3.0.7 → 3.1.2
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 +1229 -31
- package/dist/wordpress-api.js +156 -1
- package/package.json +3 -1
package/dist/mcp-server.js
CHANGED
|
@@ -14,7 +14,9 @@ import fetch from "node-fetch";
|
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import { readFileSync, statSync, realpathSync, appendFileSync, mkdirSync, existsSync, writeFileSync, renameSync } from "node:fs";
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
17
|
-
import { resolve as resolvePath, sep, dirname, join as joinPath } from "node:path";
|
|
17
|
+
import { resolve as resolvePath, sep, dirname, join as joinPath, extname } from "node:path";
|
|
18
|
+
import { marked } from "marked";
|
|
19
|
+
import { parse as parseHTML } from "node-html-parser";
|
|
18
20
|
import os from "node:os";
|
|
19
21
|
import { fileURLToPath } from "node:url";
|
|
20
22
|
// package.json からバージョンを取得
|
|
@@ -456,6 +458,280 @@ function readHTMLFromFile(filePath, maxSizeBytes = 2 * 1024 * 1024) {
|
|
|
456
458
|
return { html };
|
|
457
459
|
}
|
|
458
460
|
|
|
461
|
+
// ========================================
|
|
462
|
+
// Markdown → Gutenberg Block Converter
|
|
463
|
+
// ========================================
|
|
464
|
+
|
|
465
|
+
function _convertHeading(node) {
|
|
466
|
+
const tag = node.tagName.toLowerCase();
|
|
467
|
+
const level = parseInt(tag[1]);
|
|
468
|
+
const attrs = level === 2 ? '' : ` {"level":${level}}`;
|
|
469
|
+
return `<!-- wp:heading${attrs} -->\n<${tag} class="wp-block-heading">${node.innerHTML}</${tag}>\n<!-- /wp:heading -->`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function _convertParagraph(node) {
|
|
473
|
+
const img = node.querySelector('img');
|
|
474
|
+
if (img && node.childNodes.length === 1) {
|
|
475
|
+
return _convertImage(img);
|
|
476
|
+
}
|
|
477
|
+
return `<!-- wp:paragraph -->\n<p>${node.innerHTML}</p>\n<!-- /wp:paragraph -->`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function _convertList(node) {
|
|
481
|
+
const tag = node.tagName.toLowerCase();
|
|
482
|
+
const ordered = tag === 'ol';
|
|
483
|
+
const attrs = ordered ? ' {"ordered":true}' : '';
|
|
484
|
+
const outerTag = ordered ? 'ol' : 'ul';
|
|
485
|
+
return `<!-- wp:list${attrs} -->\n<${outerTag} class="wp-block-list">${node.innerHTML}</${outerTag}>\n<!-- /wp:list -->`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function _convertQuote(node) {
|
|
489
|
+
let inner = node.innerHTML.trim();
|
|
490
|
+
if (!inner.startsWith('<')) {
|
|
491
|
+
inner = `<p>${inner}</p>`;
|
|
492
|
+
}
|
|
493
|
+
return `<!-- wp:quote -->\n<blockquote class="wp-block-quote">${inner}</blockquote>\n<!-- /wp:quote -->`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function _convertCode(node) {
|
|
497
|
+
const inner = node.innerHTML.replace(/<code\s+class="[^"]*">/g, '<code>');
|
|
498
|
+
return `<!-- wp:code -->\n<pre class="wp-block-code">${inner}</pre>\n<!-- /wp:code -->`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function _convertTable(node) {
|
|
502
|
+
return `<!-- wp:table -->\n<figure class="wp-block-table"><table>${node.innerHTML}</table></figure>\n<!-- /wp:table -->`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function _convertImage(node) {
|
|
506
|
+
const img = node.tagName?.toLowerCase() === 'img' ? node : node.querySelector('img');
|
|
507
|
+
if (!img) return `<!-- wp:html -->\n${node.outerHTML}\n<!-- /wp:html -->`;
|
|
508
|
+
const src = img.getAttribute('src') || '';
|
|
509
|
+
const alt = img.getAttribute('alt') || '';
|
|
510
|
+
return `<!-- wp:image -->\n<figure class="wp-block-image"><img src="${src}" alt="${alt}"/></figure>\n<!-- /wp:image -->`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function htmlToGutenbergBlocks(html) {
|
|
514
|
+
const root = parseHTML(html);
|
|
515
|
+
const blocks = [];
|
|
516
|
+
for (const node of root.childNodes) {
|
|
517
|
+
if (node.nodeType === 3) {
|
|
518
|
+
const text = node.text.trim();
|
|
519
|
+
if (!text) continue;
|
|
520
|
+
blocks.push(`<!-- wp:paragraph -->\n<p>${text}</p>\n<!-- /wp:paragraph -->`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (node.nodeType !== 1) continue;
|
|
524
|
+
const tag = node.tagName?.toLowerCase();
|
|
525
|
+
switch (tag) {
|
|
526
|
+
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
|
527
|
+
blocks.push(_convertHeading(node));
|
|
528
|
+
break;
|
|
529
|
+
case 'p':
|
|
530
|
+
blocks.push(_convertParagraph(node));
|
|
531
|
+
break;
|
|
532
|
+
case 'ul': case 'ol':
|
|
533
|
+
blocks.push(_convertList(node));
|
|
534
|
+
break;
|
|
535
|
+
case 'blockquote':
|
|
536
|
+
blocks.push(_convertQuote(node));
|
|
537
|
+
break;
|
|
538
|
+
case 'pre':
|
|
539
|
+
blocks.push(_convertCode(node));
|
|
540
|
+
break;
|
|
541
|
+
case 'hr':
|
|
542
|
+
blocks.push(`<!-- wp:separator -->\n<hr class="wp-block-separator has-alpha-channel-opacity"/>\n<!-- /wp:separator -->`);
|
|
543
|
+
break;
|
|
544
|
+
case 'table':
|
|
545
|
+
blocks.push(_convertTable(node));
|
|
546
|
+
break;
|
|
547
|
+
case 'figure':
|
|
548
|
+
case 'img':
|
|
549
|
+
blocks.push(_convertImage(node));
|
|
550
|
+
break;
|
|
551
|
+
default:
|
|
552
|
+
blocks.push(`<!-- wp:html -->\n${node.outerHTML}\n<!-- /wp:html -->`);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return blocks.join('\n\n');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function resolveFileToBlockHTML(filePath) {
|
|
560
|
+
const ext = extname(filePath).toLowerCase();
|
|
561
|
+
const raw = readHTMLFromFile(filePath).html;
|
|
562
|
+
if (ext === '.md') {
|
|
563
|
+
const html = await marked.parse(raw);
|
|
564
|
+
return htmlToGutenbergBlocks(html);
|
|
565
|
+
} else if (ext === '.html' || ext === '.htm') {
|
|
566
|
+
return raw;
|
|
567
|
+
} else {
|
|
568
|
+
throw new Error(`対応していないファイル形式です: ${ext} (.md または .html のみ)`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ========================================
|
|
573
|
+
// Media Utilities
|
|
574
|
+
// ========================================
|
|
575
|
+
|
|
576
|
+
const ALLOWED_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
577
|
+
const MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
|
|
578
|
+
|
|
579
|
+
const MIME_MAP = {
|
|
580
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
|
581
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.avif': 'image/avif',
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
function validateImageFile(filePath) {
|
|
585
|
+
const resolved = resolvePath(filePath);
|
|
586
|
+
let stat;
|
|
587
|
+
try { stat = statSync(resolved); }
|
|
588
|
+
catch (e) {
|
|
589
|
+
if (e.code === 'ENOENT') throw new Error(`File not found: ${resolved}`);
|
|
590
|
+
throw new Error(`File access error: ${resolved} (${e.message})`);
|
|
591
|
+
}
|
|
592
|
+
if (stat.size > MAX_IMAGE_SIZE_BYTES) {
|
|
593
|
+
throw new Error(`File too large: ${(stat.size / 1048576).toFixed(1)}MB (max: 10MB)`);
|
|
594
|
+
}
|
|
595
|
+
const ext = resolved.slice(resolved.lastIndexOf('.')).toLowerCase();
|
|
596
|
+
if (!ALLOWED_IMAGE_EXTENSIONS.has(ext)) {
|
|
597
|
+
throw new Error(`Unsupported image format: ${ext} (allowed: ${[...ALLOWED_IMAGE_EXTENSIONS].join(', ')})`);
|
|
598
|
+
}
|
|
599
|
+
const contentType = MIME_MAP[ext] || 'application/octet-stream';
|
|
600
|
+
return { resolved, stat, ext, contentType };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ========================================
|
|
604
|
+
// Search Variant Generation (ひらがな⇔カタカナ⇔ローマ字)
|
|
605
|
+
// ========================================
|
|
606
|
+
|
|
607
|
+
const ROMAJI_MAP = {
|
|
608
|
+
'キャ': 'kya', 'キュ': 'kyu', 'キョ': 'kyo',
|
|
609
|
+
'シャ': 'sha', 'シュ': 'shu', 'ショ': 'sho',
|
|
610
|
+
'チャ': 'cha', 'チュ': 'chu', 'チョ': 'cho',
|
|
611
|
+
'ニャ': 'nya', 'ニュ': 'nyu', 'ニョ': 'nyo',
|
|
612
|
+
'ヒャ': 'hya', 'ヒュ': 'hyu', 'ヒョ': 'hyo',
|
|
613
|
+
'ミャ': 'mya', 'ミュ': 'myu', 'ミョ': 'myo',
|
|
614
|
+
'リャ': 'rya', 'リュ': 'ryu', 'リョ': 'ryo',
|
|
615
|
+
'ギャ': 'gya', 'ギュ': 'gyu', 'ギョ': 'gyo',
|
|
616
|
+
'ジャ': 'ja', 'ジュ': 'ju', 'ジョ': 'jo',
|
|
617
|
+
'ビャ': 'bya', 'ビュ': 'byu', 'ビョ': 'byo',
|
|
618
|
+
'ピャ': 'pya', 'ピュ': 'pyu', 'ピョ': 'pyo',
|
|
619
|
+
'ティ': 'ti', 'ディ': 'di', 'ファ': 'fa', 'フィ': 'fi', 'フェ': 'fe', 'フォ': 'fo',
|
|
620
|
+
'ア': 'a', 'イ': 'i', 'ウ': 'u', 'エ': 'e', 'オ': 'o',
|
|
621
|
+
'カ': 'ka', 'キ': 'ki', 'ク': 'ku', 'ケ': 'ke', 'コ': 'ko',
|
|
622
|
+
'サ': 'sa', 'シ': 'shi', 'ス': 'su', 'セ': 'se', 'ソ': 'so',
|
|
623
|
+
'タ': 'ta', 'チ': 'chi', 'ツ': 'tsu', 'テ': 'te', 'ト': 'to',
|
|
624
|
+
'ナ': 'na', 'ニ': 'ni', 'ヌ': 'nu', 'ネ': 'ne', 'ノ': 'no',
|
|
625
|
+
'ハ': 'ha', 'ヒ': 'hi', 'フ': 'fu', 'ヘ': 'he', 'ホ': 'ho',
|
|
626
|
+
'マ': 'ma', 'ミ': 'mi', 'ム': 'mu', 'メ': 'me', 'モ': 'mo',
|
|
627
|
+
'ヤ': 'ya', 'ユ': 'yu', 'ヨ': 'yo',
|
|
628
|
+
'ラ': 'ra', 'リ': 'ri', 'ル': 'ru', 'レ': 're', 'ロ': 'ro',
|
|
629
|
+
'ワ': 'wa', 'ヲ': 'wo', 'ン': 'n',
|
|
630
|
+
'ガ': 'ga', 'ギ': 'gi', 'グ': 'gu', 'ゲ': 'ge', 'ゴ': 'go',
|
|
631
|
+
'ザ': 'za', 'ジ': 'ji', 'ズ': 'zu', 'ゼ': 'ze', 'ゾ': 'zo',
|
|
632
|
+
'ダ': 'da', 'ヂ': 'di', 'ヅ': 'du', 'デ': 'de', 'ド': 'do',
|
|
633
|
+
'バ': 'ba', 'ビ': 'bi', 'ブ': 'bu', 'ベ': 'be', 'ボ': 'bo',
|
|
634
|
+
'パ': 'pa', 'ピ': 'pi', 'プ': 'pu', 'ペ': 'pe', 'ポ': 'po',
|
|
635
|
+
'ァ': 'a', 'ィ': 'i', 'ゥ': 'u', 'ェ': 'e', 'ォ': 'o',
|
|
636
|
+
'ヴ': 'v', 'ー': '', 'ッ': '',
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
function hiraToKata(text) {
|
|
640
|
+
return text.replace(/[ぁ-ゖ]/g, c => String.fromCharCode(c.charCodeAt(0) + 96));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function kataToHira(text) {
|
|
644
|
+
return text.replace(/[ァ-ヶ]/g, c => String.fromCharCode(c.charCodeAt(0) - 96));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function kataToRomaji(text) {
|
|
648
|
+
const result = [];
|
|
649
|
+
let i = 0;
|
|
650
|
+
while (i < text.length) {
|
|
651
|
+
if (i + 1 < text.length && ROMAJI_MAP[text.slice(i, i + 2)]) {
|
|
652
|
+
result.push(ROMAJI_MAP[text.slice(i, i + 2)]);
|
|
653
|
+
i += 2;
|
|
654
|
+
} else if (text[i] === 'ッ' && i + 1 < text.length) {
|
|
655
|
+
// 促音: 次の子音を重ねる
|
|
656
|
+
const next2 = (i + 2 < text.length) ? ROMAJI_MAP[text.slice(i + 1, i + 3)] : null;
|
|
657
|
+
const next1 = ROMAJI_MAP[text[i + 1]];
|
|
658
|
+
const nextRoma = next2 || next1;
|
|
659
|
+
if (nextRoma && nextRoma.length > 0) result.push(nextRoma[0]);
|
|
660
|
+
i += 1;
|
|
661
|
+
} else if (ROMAJI_MAP[text[i]] !== undefined) {
|
|
662
|
+
result.push(ROMAJI_MAP[text[i]]);
|
|
663
|
+
i += 1;
|
|
664
|
+
} else {
|
|
665
|
+
result.push(text[i]);
|
|
666
|
+
i += 1;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return result.join('');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function toRomaji(text) {
|
|
673
|
+
const kata = hiraToKata(text);
|
|
674
|
+
const romaji = kataToRomaji(kata);
|
|
675
|
+
if (/[一-鿿]/.test(romaji)) return '';
|
|
676
|
+
return romaji;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function generateSearchVariants(query, strip = [], prefixes = []) {
|
|
680
|
+
const variants = new Set();
|
|
681
|
+
let name = query;
|
|
682
|
+
variants.add(name);
|
|
683
|
+
|
|
684
|
+
if (strip && strip.length > 0) {
|
|
685
|
+
for (const suffix of strip) {
|
|
686
|
+
if (name.endsWith(suffix)) {
|
|
687
|
+
name = name.slice(0, -suffix.length);
|
|
688
|
+
variants.add(name);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const bracketMatch = name.match(/[((]([^))]+)[))]/);
|
|
695
|
+
if (bracketMatch) {
|
|
696
|
+
const reading = bracketMatch[1];
|
|
697
|
+
const base = name.replace(/[((][^))]+[))]/, '').trim();
|
|
698
|
+
variants.add(base);
|
|
699
|
+
variants.add(reading);
|
|
700
|
+
variants.add(hiraToKata(reading));
|
|
701
|
+
variants.add(kataToHira(reading));
|
|
702
|
+
const romaji = toRomaji(reading);
|
|
703
|
+
if (romaji) variants.add(romaji);
|
|
704
|
+
name = base;
|
|
705
|
+
} else {
|
|
706
|
+
variants.add(hiraToKata(name));
|
|
707
|
+
variants.add(kataToHira(name));
|
|
708
|
+
const romaji = toRomaji(name);
|
|
709
|
+
if (romaji) variants.add(romaji);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (prefixes && prefixes.length > 0) {
|
|
713
|
+
for (const prefix of prefixes) {
|
|
714
|
+
variants.add(`${prefix}${name}`);
|
|
715
|
+
if (bracketMatch) {
|
|
716
|
+
variants.add(`${prefix}${bracketMatch[1]}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
variants.delete('');
|
|
722
|
+
return [...variants];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function flattenFileBirdFolders(folders, result = []) {
|
|
726
|
+
for (const f of folders) {
|
|
727
|
+
result.push({ id: f.id, name: f.text || f.title || f.name || '', count: parseInt(f['data-count'], 10) || 0 });
|
|
728
|
+
if (f.children && f.children.length > 0) {
|
|
729
|
+
flattenFileBirdFolders(f.children, result);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
|
|
459
735
|
/**
|
|
460
736
|
* 単一クライアントの Editor/Headless 判定
|
|
461
737
|
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
@@ -679,10 +955,15 @@ async function resolvePostId(rawPostId, client, { skipConflictCheck = false } =
|
|
|
679
955
|
}
|
|
680
956
|
if (!slug) return { error: '❌ URL からスラッグを抽出できませんでした。' };
|
|
681
957
|
|
|
682
|
-
// slug → ID
|
|
683
|
-
const
|
|
958
|
+
// slug → ID 解決(投稿 + 固定ページを統合検索)
|
|
959
|
+
const searchResult = await client.searchPosts({ slug, status: 'any', post_type: 'any' });
|
|
960
|
+
const posts = searchResult.items || [];
|
|
684
961
|
if (!posts || posts.length === 0) {
|
|
685
|
-
return { error: `❌ スラッグ "${slug}"
|
|
962
|
+
return { error: `❌ スラッグ "${slug}" に一致する投稿・固定ページが見つかりません。` };
|
|
963
|
+
}
|
|
964
|
+
if (posts.length > 1) {
|
|
965
|
+
const candidates = posts.map(p => ` [${p.id}] ${p.post_type === 'page' ? '[固定ページ]' : '[投稿]'} ${p.title || '(無題)'} — ${p.link || '-'}`).join('\n');
|
|
966
|
+
return { error: `❌ スラッグ "${slug}" に複数の候補が見つかりました。postId を数値で指定してください:\n${candidates}` };
|
|
686
967
|
}
|
|
687
968
|
const resolvedId = posts[0].id;
|
|
688
969
|
|
|
@@ -2161,7 +2442,7 @@ const tools = [
|
|
|
2161
2442
|
},
|
|
2162
2443
|
{
|
|
2163
2444
|
name: "update_post_meta",
|
|
2164
|
-
description: "Update post metadata.",
|
|
2445
|
+
description: "Update post or page metadata.",
|
|
2165
2446
|
inputSchema: {
|
|
2166
2447
|
type: "object",
|
|
2167
2448
|
properties: {
|
|
@@ -2170,16 +2451,39 @@ const tools = [
|
|
|
2170
2451
|
title: { type: "string", description: "Title" },
|
|
2171
2452
|
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Status" },
|
|
2172
2453
|
slug: { type: "string", description: "Slug (URL)" },
|
|
2173
|
-
categories: { type: "array", items: { type: "number" }, description: "Category IDs" },
|
|
2174
|
-
tags: { type: "array", items: { type: "number" }, description: "Tag IDs" },
|
|
2454
|
+
categories: { type: "array", items: { type: "number" }, description: "Category IDs (posts only)" },
|
|
2455
|
+
tags: { type: "array", items: { type: "number" }, description: "Tag IDs (posts only)" },
|
|
2456
|
+
parent: { type: "number", description: "Parent page ID (pages only, 0 to remove)" },
|
|
2175
2457
|
excerpt: { type: "string", description: "Excerpt" },
|
|
2176
2458
|
featured_media: { type: "number", description: "Featured image ID (0 to remove)" },
|
|
2177
2459
|
},
|
|
2178
2460
|
},
|
|
2179
2461
|
},
|
|
2462
|
+
{
|
|
2463
|
+
name: "create_post",
|
|
2464
|
+
description: "Create a new WordPress post or page (draft by default). Returns post ID and editor URL.",
|
|
2465
|
+
inputSchema: {
|
|
2466
|
+
type: "object",
|
|
2467
|
+
properties: {
|
|
2468
|
+
title: { type: "string", description: "Post title" },
|
|
2469
|
+
post_type: { type: "string", enum: ["post", "page"], description: "Content type (default: post)" },
|
|
2470
|
+
slug: { type: "string", description: "URL slug" },
|
|
2471
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Status (default: draft)" },
|
|
2472
|
+
categories: { type: "array", items: { type: "number" }, description: "Category IDs (posts only)" },
|
|
2473
|
+
tags: { type: "array", items: { type: "number" }, description: "Tag IDs (posts only)" },
|
|
2474
|
+
parent: { type: "number", description: "Parent page ID (pages only)" },
|
|
2475
|
+
featured_media: { type: "number", description: "Featured image media ID" },
|
|
2476
|
+
excerpt: { type: "string", description: "Excerpt" },
|
|
2477
|
+
content: { type: "string", description: "HTML content (exclusive with filePath)" },
|
|
2478
|
+
filePath: { type: "string", description: "Local file path (.md or .html, exclusive with content)" },
|
|
2479
|
+
site: siteParam,
|
|
2480
|
+
},
|
|
2481
|
+
required: ["title"],
|
|
2482
|
+
},
|
|
2483
|
+
},
|
|
2180
2484
|
{
|
|
2181
2485
|
name: "list_posts",
|
|
2182
|
-
description: "Search posts. Use slug for exact match, search for keyword. Mutually exclusive.",
|
|
2486
|
+
description: "Search posts and pages. Use slug for exact match, search for keyword. Mutually exclusive.",
|
|
2183
2487
|
inputSchema: {
|
|
2184
2488
|
type: "object",
|
|
2185
2489
|
properties: {
|
|
@@ -2187,6 +2491,7 @@ const tools = [
|
|
|
2187
2491
|
slug: { type: "string", description: "Slug for exact match (exclusive with search)" },
|
|
2188
2492
|
search: { type: "string", description: "Keyword (exclusive with slug)" },
|
|
2189
2493
|
status: { type: "string", enum: ["publish", "draft", "pending", "private", "any"], description: "Status filter (default: publish)" },
|
|
2494
|
+
post_type: { type: "string", enum: ["post", "page", "any"], description: "Filter by type: post, page, or any (default: any)" },
|
|
2190
2495
|
categories: { type: "array", items: { type: "number" }, description: "Category IDs" },
|
|
2191
2496
|
tags: { type: "array", items: { type: "number" }, description: "Tag IDs" },
|
|
2192
2497
|
per_page: { type: "integer", description: "Per page (1-100)", minimum: 1, maximum: 100 },
|
|
@@ -2196,6 +2501,101 @@ const tools = [
|
|
|
2196
2501
|
},
|
|
2197
2502
|
},
|
|
2198
2503
|
},
|
|
2504
|
+
{
|
|
2505
|
+
name: "list_blog_parts",
|
|
2506
|
+
description: "List or search blog parts (reusable blocks). When partsId is specified, returns posts/pages that directly use that blog parts. SWELL theme only.",
|
|
2507
|
+
inputSchema: {
|
|
2508
|
+
type: "object",
|
|
2509
|
+
properties: {
|
|
2510
|
+
site: siteParam,
|
|
2511
|
+
partsId: { type: "integer", description: "Blog parts ID. When specified, returns posts/pages that directly use this blog parts instead of listing blog parts." },
|
|
2512
|
+
search: { type: "string", description: "Keyword search (ignored when partsId is set)" },
|
|
2513
|
+
status: { type: "string", enum: ["publish", "draft", "private", "any"], description: "Status filter (default: publish, ignored when partsId is set)" },
|
|
2514
|
+
per_page: { type: "integer", description: "Per page (1-100)", minimum: 1, maximum: 100 },
|
|
2515
|
+
page: { type: "integer", description: "Page number", minimum: 1 },
|
|
2516
|
+
},
|
|
2517
|
+
},
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
name: "get_blog_parts",
|
|
2521
|
+
description: "Get blog parts structure and blocks by partsId. Opens a blog parts post for viewing/editing.",
|
|
2522
|
+
inputSchema: {
|
|
2523
|
+
type: "object",
|
|
2524
|
+
properties: {
|
|
2525
|
+
partsId: { type: "integer", description: "Blog parts ID (required)" },
|
|
2526
|
+
site: siteParam,
|
|
2527
|
+
full: { type: "boolean", description: "Full block data" },
|
|
2528
|
+
section: { type: "string", description: "Zoom into section" },
|
|
2529
|
+
blockType: { type: "string", description: "Filter by block type" },
|
|
2530
|
+
contains: { type: "string", description: "Text search" },
|
|
2531
|
+
},
|
|
2532
|
+
required: ["partsId"],
|
|
2533
|
+
},
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
name: "create_blog_parts",
|
|
2537
|
+
description: "Create a new blog parts (reusable block). SWELL theme only. Returns partsId and editor URL. To insert into an article, use insert_blog_parts with the returned partsId.",
|
|
2538
|
+
inputSchema: {
|
|
2539
|
+
type: "object",
|
|
2540
|
+
properties: {
|
|
2541
|
+
title: { type: "string", description: "Blog parts title" },
|
|
2542
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Status (default: publish)" },
|
|
2543
|
+
slug: { type: "string", description: "URL slug" },
|
|
2544
|
+
content: { type: "string", description: "HTML content (exclusive with filePath)" },
|
|
2545
|
+
filePath: { type: "string", description: "Local file path (.md or .html, exclusive with content)" },
|
|
2546
|
+
parts_use: { type: "array", items: { type: "number" }, description: "parts_use taxonomy term IDs (SWELL usage category)" },
|
|
2547
|
+
site: siteParam,
|
|
2548
|
+
},
|
|
2549
|
+
required: ["title"],
|
|
2550
|
+
},
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: "update_blog_parts",
|
|
2554
|
+
description: "Update blog parts blocks. Same as update_blocks but for blog parts posts. Headless only.",
|
|
2555
|
+
inputSchema: {
|
|
2556
|
+
type: "object",
|
|
2557
|
+
properties: {
|
|
2558
|
+
partsId: { type: "integer", description: "Blog parts ID (required)" },
|
|
2559
|
+
site: siteParam,
|
|
2560
|
+
snapshotId: { type: "string", description: "Snapshot ID from get_blog_parts." },
|
|
2561
|
+
ref: { type: "string", description: "Block ref from snapshot." },
|
|
2562
|
+
refs: { type: "array", items: { type: "string" }, description: "Multiple block refs." },
|
|
2563
|
+
expectedRevision: { type: "string", description: "Revision check." },
|
|
2564
|
+
newHTML: { type: "string", description: "New block HTML." },
|
|
2565
|
+
replacements: {
|
|
2566
|
+
type: "array",
|
|
2567
|
+
items: {
|
|
2568
|
+
type: "object",
|
|
2569
|
+
properties: {
|
|
2570
|
+
old: { type: "string" },
|
|
2571
|
+
new: { type: "string" },
|
|
2572
|
+
regex: { type: "boolean" },
|
|
2573
|
+
},
|
|
2574
|
+
required: ["old", "new"],
|
|
2575
|
+
},
|
|
2576
|
+
description: "Text replacements.",
|
|
2577
|
+
},
|
|
2578
|
+
},
|
|
2579
|
+
required: ["partsId"],
|
|
2580
|
+
},
|
|
2581
|
+
},
|
|
2582
|
+
{
|
|
2583
|
+
name: "insert_blog_parts",
|
|
2584
|
+
description: "Insert a blog parts block into an article. Builds the correct block HTML automatically.",
|
|
2585
|
+
inputSchema: {
|
|
2586
|
+
type: "object",
|
|
2587
|
+
properties: {
|
|
2588
|
+
partsId: { type: "integer", description: "Blog parts ID (required)" },
|
|
2589
|
+
postId: postIdParam,
|
|
2590
|
+
site: siteParam,
|
|
2591
|
+
snapshotId: { type: "string", description: "Snapshot ID (from get_article_structure or any write response). Required when using beforeRef/afterRef. Omit to append to end." },
|
|
2592
|
+
expectedRevision: { type: "string", description: "Revision from snapshot. Rejects if structure changed." },
|
|
2593
|
+
beforeRef: { type: "string", description: "Insert before this ref." },
|
|
2594
|
+
afterRef: { type: "string", description: "Insert after this ref." },
|
|
2595
|
+
},
|
|
2596
|
+
required: ["partsId"],
|
|
2597
|
+
},
|
|
2598
|
+
},
|
|
2199
2599
|
{
|
|
2200
2600
|
name: "list_taxonomies",
|
|
2201
2601
|
description: "List categories or tags.",
|
|
@@ -2244,7 +2644,7 @@ const tools = [
|
|
|
2244
2644
|
},
|
|
2245
2645
|
{
|
|
2246
2646
|
name: "get_block_html",
|
|
2247
|
-
description: "Get block HTML. Use before newHTML to safely edit custom/theme blocks.",
|
|
2647
|
+
description: "Get block HTML. Use before newHTML to safely edit custom/theme blocks. Blog parts (loos/blog-parts) are auto-expanded to show inner content. To edit blog parts content, use update_blog_parts.",
|
|
2248
2648
|
inputSchema: {
|
|
2249
2649
|
type: "object",
|
|
2250
2650
|
properties: {
|
|
@@ -2418,6 +2818,46 @@ const tools = [
|
|
|
2418
2818
|
},
|
|
2419
2819
|
},
|
|
2420
2820
|
},
|
|
2821
|
+
{
|
|
2822
|
+
name: "upload_media",
|
|
2823
|
+
description: "Upload image to WordPress media library. Returns media ID, URL, dimensions.",
|
|
2824
|
+
inputSchema: {
|
|
2825
|
+
type: "object",
|
|
2826
|
+
properties: {
|
|
2827
|
+
site: siteParam,
|
|
2828
|
+
filePath: { type: "string", description: "Absolute path to image file on local machine" },
|
|
2829
|
+
name: { type: "string", description: "New filename (without extension, ASCII/hyphens recommended)" },
|
|
2830
|
+
alt: { type: "string", description: "Alt text and title (Japanese OK)" },
|
|
2831
|
+
title: { type: "string", description: "Title (defaults to alt if omitted)" },
|
|
2832
|
+
},
|
|
2833
|
+
required: ["filePath", "name", "alt"],
|
|
2834
|
+
},
|
|
2835
|
+
},
|
|
2836
|
+
{
|
|
2837
|
+
name: "search_media",
|
|
2838
|
+
description: "Search WordPress media library. Auto-generates hiragana/katakana/romaji variants for broader matching.",
|
|
2839
|
+
inputSchema: {
|
|
2840
|
+
type: "object",
|
|
2841
|
+
properties: {
|
|
2842
|
+
site: siteParam,
|
|
2843
|
+
query: { type: "string", description: "Search keyword" },
|
|
2844
|
+
folder: { type: "string", description: "FileBird folder name (partial match). Filters media to this folder." },
|
|
2845
|
+
strip: { type: "array", items: { type: "string" }, description: "Suffixes to strip (e.g. ['先生', 'さん'])" },
|
|
2846
|
+
prefix: { type: "array", items: { type: "string" }, description: "Prefixes to add (e.g. ['エキサイト-'])" },
|
|
2847
|
+
per_page: { type: "integer", description: "Results per variant (1-20, default: 10)", minimum: 1, maximum: 20 },
|
|
2848
|
+
},
|
|
2849
|
+
},
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
name: "list_media_folders",
|
|
2853
|
+
description: "List FileBird media folders. Returns folder names, IDs, and file counts.",
|
|
2854
|
+
inputSchema: {
|
|
2855
|
+
type: "object",
|
|
2856
|
+
properties: {
|
|
2857
|
+
site: siteParam,
|
|
2858
|
+
},
|
|
2859
|
+
},
|
|
2860
|
+
},
|
|
2421
2861
|
{
|
|
2422
2862
|
name: "list_connections",
|
|
2423
2863
|
description: "List connections.",
|
|
@@ -2449,6 +2889,39 @@ const tools = [
|
|
|
2449
2889
|
required: ["content"],
|
|
2450
2890
|
},
|
|
2451
2891
|
},
|
|
2892
|
+
{
|
|
2893
|
+
name: "search_asp_link",
|
|
2894
|
+
description: "Search registered ASP affiliate links. Search by title, slug, or keyword across title and asp fields.",
|
|
2895
|
+
inputSchema: {
|
|
2896
|
+
type: "object",
|
|
2897
|
+
properties: {
|
|
2898
|
+
site: siteParam,
|
|
2899
|
+
search: { type: "string", description: "Keyword search (title, asp_id, asp_name)" },
|
|
2900
|
+
slug: { type: "string", description: "Exact post slug match (exclusive with search)" },
|
|
2901
|
+
per_page: { type: "integer", minimum: 1, maximum: 100, description: "Results per page (default: 20)" },
|
|
2902
|
+
page: { type: "integer", minimum: 1, description: "Page number (default: 1)" },
|
|
2903
|
+
},
|
|
2904
|
+
},
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
name: "register_asp_link",
|
|
2908
|
+
description: "Register a new ASP affiliate link. Creates a jump post with ASP metadata.",
|
|
2909
|
+
inputSchema: {
|
|
2910
|
+
type: "object",
|
|
2911
|
+
properties: {
|
|
2912
|
+
site: siteParam,
|
|
2913
|
+
title: { type: "string", description: "Display name (e.g. 'モコム')" },
|
|
2914
|
+
slug: { type: "string", description: "WordPress post slug for URL (e.g. 'mocom')" },
|
|
2915
|
+
asp_id: { type: "string", description: "ASP案件ID — data-id attribute used by JSLinkHelper (defaults to slug if omitted)" },
|
|
2916
|
+
asp_name: { type: "string", description: "案件名 (defaults to title if omitted)" },
|
|
2917
|
+
asp_url: { type: "string", description: "ASP affiliate URL (required)" },
|
|
2918
|
+
asp_url_sp: { type: "string", description: "ASP URL (B) for A/B testing (optional)" },
|
|
2919
|
+
direct_url: { type: "string", description: "Direct URL bypassing ASP (optional)" },
|
|
2920
|
+
permalink: { type: "string", description: "WordPress permalink slug (defaults to asp_id, then slug if omitted)" },
|
|
2921
|
+
},
|
|
2922
|
+
required: ["title", "slug", "asp_url"],
|
|
2923
|
+
},
|
|
2924
|
+
},
|
|
2452
2925
|
];
|
|
2453
2926
|
|
|
2454
2927
|
// ツールリストのハンドラ
|
|
@@ -2576,7 +3049,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2576
3049
|
return { content: [{ type: "text", text: "❌ filePath は attributeUpdates と併用できません。" }], isError: true };
|
|
2577
3050
|
}
|
|
2578
3051
|
try {
|
|
2579
|
-
newHTML =
|
|
3052
|
+
newHTML = await resolveFileToBlockHTML(args.filePath);
|
|
2580
3053
|
} catch (e) {
|
|
2581
3054
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
2582
3055
|
}
|
|
@@ -2846,7 +3319,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2846
3319
|
}
|
|
2847
3320
|
|
|
2848
3321
|
// ツール実行のハンドラ
|
|
2849
|
-
const _WRITE_TOOLS = new Set(['update_blocks', 'delete_block', 'move_block', 'duplicate_block', 'insert_block', 'table_operations']);
|
|
3322
|
+
const _WRITE_TOOLS = new Set(['update_blocks', 'delete_block', 'move_block', 'duplicate_block', 'insert_block', 'table_operations', 'update_blog_parts', 'insert_blog_parts']);
|
|
2850
3323
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2851
3324
|
const { name, arguments: args } = request.params;
|
|
2852
3325
|
const _toolLogStart = Date.now();
|
|
@@ -2967,10 +3440,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2967
3440
|
const endIdx = (limit && limit > 0) ? startIdx + limit : blocks.length;
|
|
2968
3441
|
blocks = blocks.slice(startIdx, endIdx);
|
|
2969
3442
|
|
|
3443
|
+
// エディタモード時、ブロパの展開情報をヘッドレス API で補完
|
|
3444
|
+
if (mode === 'editor' && client) {
|
|
3445
|
+
for (const b of blocks) {
|
|
3446
|
+
if (b.type === 'loos/blog-parts' && !b.blogParts) {
|
|
3447
|
+
const pid = parseInt(b.attributes?.partsID, 10);
|
|
3448
|
+
if (pid > 0) {
|
|
3449
|
+
try {
|
|
3450
|
+
const bpBlocks = await client.headlessGetBlocks(pid);
|
|
3451
|
+
const meta = await client.headlessGetMeta(pid);
|
|
3452
|
+
b.blogParts = {
|
|
3453
|
+
partsId: pid,
|
|
3454
|
+
partsTitle: meta?.title || '',
|
|
3455
|
+
blocks: (bpBlocks?.blocks || []).map(bl => ({ type: bl.type, depth: bl.depth || 0 })),
|
|
3456
|
+
};
|
|
3457
|
+
} catch (_) {
|
|
3458
|
+
b.blogPartsError = 'not_found';
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
|
|
2970
3465
|
const blockList = blocks.map(b => {
|
|
2971
3466
|
const attrPreview = JSON.stringify(b.attributes || {}).slice(0, 200);
|
|
2972
3467
|
const t = formatTreeLine(b);
|
|
2973
|
-
|
|
3468
|
+
let line = `${t.indent}[${b.index}${_refTag(b.index)}] ${t.marker}${b.type}${t.childInfo} - ${b.section || "(記事冒頭)"} ${t.depthInfo}\n${t.indent} ${attrPreview}${attrPreview.length >= 200 ? '...' : ''}`;
|
|
3469
|
+
|
|
3470
|
+
// ブロパ展開表示(読み取り専用、ref なし)
|
|
3471
|
+
if (b.blogParts) {
|
|
3472
|
+
line += `\n${t.indent} 📦 "${b.blogParts.partsTitle}" [ID:${b.blogParts.partsId}]`;
|
|
3473
|
+
const innerBlocks = b.blogParts.blocks || [];
|
|
3474
|
+
for (const inner of innerBlocks.slice(0, 10)) {
|
|
3475
|
+
const innerIndent = ' '.repeat((inner.depth || 0) + 1);
|
|
3476
|
+
line += `\n${t.indent} ${innerIndent}└ ${inner.type}`;
|
|
3477
|
+
}
|
|
3478
|
+
if (innerBlocks.length > 10) {
|
|
3479
|
+
line += `\n${t.indent} ... 他 ${innerBlocks.length - 10} ブロック`;
|
|
3480
|
+
}
|
|
3481
|
+
} else if (b.blogPartsError) {
|
|
3482
|
+
line += `\n${t.indent} ⚠️ ブログパーツ展開失敗: ${b.blogPartsError}`;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
return line;
|
|
2974
3486
|
}).join("\n\n");
|
|
2975
3487
|
|
|
2976
3488
|
let paginationInfo = '';
|
|
@@ -3313,6 +3825,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3313
3825
|
output += `${indent}${prefix} ${heading.text} (H${heading.level}, index:${heading.index}${_refTag(heading.index)})\n`;
|
|
3314
3826
|
}
|
|
3315
3827
|
|
|
3828
|
+
// ブログパーツ一覧
|
|
3829
|
+
if (state.blockSummary && state.blockSummary['loos/blog-parts']) {
|
|
3830
|
+
const bpEntries = state.blockSummary['loos/blog-parts'].blocks || [];
|
|
3831
|
+
const bpItems = [];
|
|
3832
|
+
for (const bp of bpEntries) {
|
|
3833
|
+
const pid = bp.partsId || (bp.attributes?.partsID ? parseInt(bp.attributes.partsID, 10) : 0);
|
|
3834
|
+
if (!pid) continue;
|
|
3835
|
+
let title = bp.partsTitle || '';
|
|
3836
|
+
// エディタモード等でタイトルが空の場合、ヘッドレス API で補完
|
|
3837
|
+
if (!title && client) {
|
|
3838
|
+
try {
|
|
3839
|
+
const meta = await client.headlessGetMeta(pid);
|
|
3840
|
+
title = meta?.title || '';
|
|
3841
|
+
} catch (_) {}
|
|
3842
|
+
}
|
|
3843
|
+
bpItems.push({ index: bp.index, partsId: pid, title });
|
|
3844
|
+
}
|
|
3845
|
+
if (bpItems.length > 0) {
|
|
3846
|
+
output += `\nブログパーツ:\n`;
|
|
3847
|
+
for (const bp of bpItems) {
|
|
3848
|
+
output += ` [${bp.index}] 📦 "${bp.title || '(不明)'}" [ID:${bp.partsId}]\n`;
|
|
3849
|
+
}
|
|
3850
|
+
output += ` ※ 中身の確認は get_blog_parts(partsId) を使用\n`;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3316
3854
|
return {
|
|
3317
3855
|
content: [{ type: "text", text: output + _snapshotLine + _modeTag_gas }],
|
|
3318
3856
|
};
|
|
@@ -3751,21 +4289,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3751
4289
|
const statusInfo = mode === 'editor'
|
|
3752
4290
|
? `モード: Editor (接続中)`
|
|
3753
4291
|
: `モード: Headless (postId: ${postId})`;
|
|
3754
|
-
const
|
|
3755
|
-
|
|
4292
|
+
const isPage = m.post_type === 'page';
|
|
4293
|
+
const typeLabel = isPage ? '📄 固定ページメタ情報' : '📄 投稿メタ情報';
|
|
4294
|
+
const lines = [
|
|
4295
|
+
typeLabel,
|
|
3756
4296
|
``,
|
|
3757
4297
|
statusInfo,
|
|
3758
4298
|
`ID: ${m.id}`,
|
|
4299
|
+
`タイプ: ${m.post_type || 'post'}`,
|
|
3759
4300
|
`タイトル: ${m.title}`,
|
|
3760
4301
|
`ステータス: ${m.status}`,
|
|
3761
4302
|
`スラッグ: ${m.slug}`,
|
|
3762
4303
|
`URL: ${m.link}`,
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
4304
|
+
];
|
|
4305
|
+
if (isPage) {
|
|
4306
|
+
lines.push(`親ページ: ${m.parent || 'なし'}`);
|
|
4307
|
+
} else {
|
|
4308
|
+
lines.push(`カテゴリ: ${JSON.stringify(m.categories)}`);
|
|
4309
|
+
lines.push(`タグ: ${JSON.stringify(m.tags)}`);
|
|
4310
|
+
}
|
|
4311
|
+
lines.push(`アイキャッチ: ${m.featuredImage || 'なし'}`);
|
|
4312
|
+
lines.push(`抜粋: ${m.excerpt || 'なし'}`);
|
|
4313
|
+
lines.push(`更新日: ${m.modified}`);
|
|
4314
|
+
const text = lines.join('\n');
|
|
3769
4315
|
return { content: [{ type: "text", text }] };
|
|
3770
4316
|
}
|
|
3771
4317
|
|
|
@@ -3791,6 +4337,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3791
4337
|
if (args.slug !== undefined) updateData.slug = args.slug;
|
|
3792
4338
|
if (args.categories !== undefined) updateData.categories = args.categories;
|
|
3793
4339
|
if (args.tags !== undefined) updateData.tags = args.tags;
|
|
4340
|
+
if (args.parent !== undefined) updateData.parent = args.parent;
|
|
3794
4341
|
if (args.excerpt !== undefined) updateData.excerpt = args.excerpt;
|
|
3795
4342
|
if (args.featured_media !== undefined) updateData.featured_media = args.featured_media;
|
|
3796
4343
|
|
|
@@ -3812,6 +4359,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3812
4359
|
return { content: [{ type: "text", text: updateText }] };
|
|
3813
4360
|
}
|
|
3814
4361
|
|
|
4362
|
+
case "create_post": {
|
|
4363
|
+
let client;
|
|
4364
|
+
try {
|
|
4365
|
+
client = registry.get(args?.site);
|
|
4366
|
+
} catch (e) {
|
|
4367
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
const title = args?.title?.trim();
|
|
4371
|
+
if (!title) {
|
|
4372
|
+
return { content: [{ type: "text", text: "❌ title は必須です。" }], isError: true };
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
let content = args?.content;
|
|
4376
|
+
const filePath = args?.filePath;
|
|
4377
|
+
|
|
4378
|
+
if (content !== undefined && filePath !== undefined) {
|
|
4379
|
+
return { content: [{ type: "text", text: "❌ content と filePath は同時に指定できません。どちらか一方を指定してください。" }], isError: true };
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
if (filePath) {
|
|
4383
|
+
try {
|
|
4384
|
+
content = await resolveFileToBlockHTML(filePath);
|
|
4385
|
+
} catch (e) {
|
|
4386
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
const postType = args?.post_type || 'post';
|
|
4391
|
+
|
|
4392
|
+
if (postType === 'page' && (args?.categories !== undefined || args?.tags !== undefined)) {
|
|
4393
|
+
return { content: [{ type: "text", text: "❌ 固定ページにカテゴリ・タグは設定できません。" }], isError: true };
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
const postData = {
|
|
4397
|
+
title,
|
|
4398
|
+
status: args?.status || 'draft',
|
|
4399
|
+
post_type: postType,
|
|
4400
|
+
};
|
|
4401
|
+
if (args?.slug !== undefined) postData.slug = args.slug;
|
|
4402
|
+
if (postType === 'page') {
|
|
4403
|
+
if (args?.parent !== undefined) postData.parent = args.parent;
|
|
4404
|
+
} else {
|
|
4405
|
+
if (args?.categories !== undefined) postData.categories = args.categories;
|
|
4406
|
+
if (args?.tags !== undefined) postData.tags = args.tags;
|
|
4407
|
+
}
|
|
4408
|
+
if (args?.excerpt !== undefined) postData.excerpt = args.excerpt;
|
|
4409
|
+
if (args?.featured_media !== undefined) postData.featured_media = args.featured_media;
|
|
4410
|
+
if (content && content.trim()) postData.content = content;
|
|
4411
|
+
|
|
4412
|
+
const typeLabel = postType === 'page' ? '固定ページ' : '投稿';
|
|
4413
|
+
|
|
4414
|
+
try {
|
|
4415
|
+
const created = await client.createPost(postData);
|
|
4416
|
+
const postId = created.id;
|
|
4417
|
+
const editUrl = `${client.wpUrl.replace(/\/$/, '')}/wp-admin/post.php?post=${postId}&action=edit`;
|
|
4418
|
+
const resultText = [
|
|
4419
|
+
`✅ ${typeLabel}を作成しました`,
|
|
4420
|
+
``,
|
|
4421
|
+
` ID: ${postId}`,
|
|
4422
|
+
` タイプ: ${postType}`,
|
|
4423
|
+
` タイトル: ${created.title?.raw || title}`,
|
|
4424
|
+
` ステータス: ${created.status}`,
|
|
4425
|
+
` 編集URL: ${editUrl}`,
|
|
4426
|
+
].join('\n');
|
|
4427
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
4428
|
+
} catch (e) {
|
|
4429
|
+
return errorResponse(name, `${typeLabel}作成に失敗しました: ${e.message}`, args?.site);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
|
|
3815
4433
|
case "list_posts": {
|
|
3816
4434
|
let client;
|
|
3817
4435
|
try {
|
|
@@ -3829,26 +4447,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3829
4447
|
return { content: [{ type: "text", text: "❌ slug と search は同時に指定できません。slug(完全一致)または search(キーワード検索)のいずれかを使用してください。" }], isError: true };
|
|
3830
4448
|
}
|
|
3831
4449
|
|
|
3832
|
-
|
|
3833
|
-
const listArgs = { ...(args || {}), slug, search };
|
|
4450
|
+
const postType = args?.post_type || 'any';
|
|
4451
|
+
const listArgs = { ...(args || {}), slug, search, post_type: postType };
|
|
3834
4452
|
if (slug && !listArgs.status) {
|
|
3835
4453
|
listArgs.status = 'any';
|
|
3836
4454
|
}
|
|
3837
4455
|
|
|
3838
|
-
const
|
|
4456
|
+
const result = await client.searchPosts(listArgs);
|
|
4457
|
+
const posts = result.items || [];
|
|
4458
|
+
const total = result.total || 0;
|
|
4459
|
+
const totalPages = result.total_pages || 0;
|
|
3839
4460
|
|
|
3840
|
-
if (
|
|
3841
|
-
return { content: [{ type: "text", text: "📋
|
|
4461
|
+
if (posts.length === 0) {
|
|
4462
|
+
return { content: [{ type: "text", text: "📋 該当する投稿・固定ページはありません。" }] };
|
|
3842
4463
|
}
|
|
3843
4464
|
|
|
3844
4465
|
const currentPage = args?.page || 1;
|
|
3845
4466
|
const list = posts.map(p => {
|
|
3846
|
-
|
|
4467
|
+
const typeLabel = p.post_type === 'page' ? '[固定ページ]' : '[投稿]';
|
|
4468
|
+
return ` [${p.id}] ${typeLabel} ${p.title || '(無題)'}\n` +
|
|
3847
4469
|
` ステータス: ${p.status} | 更新: ${p.modified?.split('T')[0] || '-'}\n` +
|
|
3848
4470
|
` URL: ${p.link || '-'}`;
|
|
3849
4471
|
}).join('\n\n');
|
|
3850
4472
|
|
|
3851
|
-
let listText = `📋
|
|
4473
|
+
let listText = `📋 一覧 (${posts.length}件 / 全${total}件)\n\n${list}`;
|
|
3852
4474
|
if (totalPages > 1) {
|
|
3853
4475
|
listText += `\n\n📄 ページ: ${currentPage} / ${totalPages}`;
|
|
3854
4476
|
if (currentPage < totalPages) {
|
|
@@ -3859,6 +4481,269 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3859
4481
|
return { content: [{ type: "text", text: listText }] };
|
|
3860
4482
|
}
|
|
3861
4483
|
|
|
4484
|
+
case "create_blog_parts": {
|
|
4485
|
+
let client;
|
|
4486
|
+
try {
|
|
4487
|
+
client = registry.get(args?.site);
|
|
4488
|
+
} catch (e) {
|
|
4489
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
const title = args?.title?.trim();
|
|
4493
|
+
if (!title) {
|
|
4494
|
+
return { content: [{ type: "text", text: "❌ title は必須です。" }], isError: true };
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
let content = args?.content;
|
|
4498
|
+
const filePath = args?.filePath;
|
|
4499
|
+
|
|
4500
|
+
if (content !== undefined && filePath !== undefined) {
|
|
4501
|
+
return { content: [{ type: "text", text: "❌ content と filePath は同時に指定できません。" }], isError: true };
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
if (filePath) {
|
|
4505
|
+
try {
|
|
4506
|
+
content = await resolveFileToBlockHTML(filePath);
|
|
4507
|
+
} catch (e) {
|
|
4508
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
const postData = {
|
|
4513
|
+
title,
|
|
4514
|
+
status: args?.status || 'publish',
|
|
4515
|
+
post_type: 'blog_parts',
|
|
4516
|
+
};
|
|
4517
|
+
if (args?.slug !== undefined) postData.slug = args.slug;
|
|
4518
|
+
if (args?.parts_use !== undefined) postData.parts_use = args.parts_use;
|
|
4519
|
+
if (content && content.trim()) postData.content = content;
|
|
4520
|
+
|
|
4521
|
+
try {
|
|
4522
|
+
const created = await client.createPost(postData);
|
|
4523
|
+
const postId = created.id;
|
|
4524
|
+
const editUrl = `${client.wpUrl.replace(/\/$/, '')}/wp-admin/post.php?post=${postId}&action=edit`;
|
|
4525
|
+
const resultText = [
|
|
4526
|
+
`✅ ブログパーツを作成しました`,
|
|
4527
|
+
``,
|
|
4528
|
+
` ID: ${postId}`,
|
|
4529
|
+
` タイトル: ${created.title?.raw || title}`,
|
|
4530
|
+
` ステータス: ${created.status}`,
|
|
4531
|
+
` 編集URL: ${editUrl}`,
|
|
4532
|
+
].join('\n');
|
|
4533
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
4534
|
+
} catch (e) {
|
|
4535
|
+
return errorResponse(name, `ブログパーツ作成に失敗しました: ${e.message}`, args?.site);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
case "list_blog_parts": {
|
|
4540
|
+
let client;
|
|
4541
|
+
try {
|
|
4542
|
+
client = registry.get(args?.site);
|
|
4543
|
+
} catch (e) {
|
|
4544
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
const bpResult = await client.searchBlogParts(args || {});
|
|
4548
|
+
|
|
4549
|
+
if (args?.partsId) {
|
|
4550
|
+
const usedBy = bpResult.usedBy || [];
|
|
4551
|
+
if (usedBy.length === 0) {
|
|
4552
|
+
return { content: [{ type: "text", text: `📋 ブログパーツ [${bpResult.partsId}] "${bpResult.partsTitle}" はどの記事でも使用されていません。` }] };
|
|
4553
|
+
}
|
|
4554
|
+
const usageList = usedBy.map(p =>
|
|
4555
|
+
` [${p.postId}] ${p.title || '(無題)'}\n ステータス: ${p.status} | 更新: ${p.modified || '-'}`
|
|
4556
|
+
).join('\n\n');
|
|
4557
|
+
let usageText = `📋 ブログパーツ [${bpResult.partsId}] "${bpResult.partsTitle}" の直接使用箇所 (${usedBy.length}件 / 全${bpResult.total}件)\n\n${usageList}`;
|
|
4558
|
+
if (bpResult.total_pages > 1) {
|
|
4559
|
+
const pg = args?.page || 1;
|
|
4560
|
+
usageText += `\n\n📄 ページ: ${pg} / ${bpResult.total_pages}`;
|
|
4561
|
+
if (pg < bpResult.total_pages) usageText += ` (次: page=${pg + 1})`;
|
|
4562
|
+
}
|
|
4563
|
+
return { content: [{ type: "text", text: usageText }] };
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
const bpParts = bpResult.items || [];
|
|
4567
|
+
const bpTotal = bpResult.total || 0;
|
|
4568
|
+
const bpTotalPages = bpResult.total_pages || 0;
|
|
4569
|
+
|
|
4570
|
+
if (bpParts.length === 0) {
|
|
4571
|
+
return { content: [{ type: "text", text: "📋 該当するブログパーツはありません。" }] };
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
const bpPage = args?.page || 1;
|
|
4575
|
+
const bpList = bpParts.map(p => {
|
|
4576
|
+
return ` [${p.id}] ${p.title || '(無題)'}\n ステータス: ${p.status} | 更新: ${p.modified || '-'}`;
|
|
4577
|
+
}).join('\n\n');
|
|
4578
|
+
|
|
4579
|
+
let bpText = `📋 ブログパーツ一覧 (${bpParts.length}件 / 全${bpTotal}件)\n\n${bpList}`;
|
|
4580
|
+
if (bpTotalPages > 1) {
|
|
4581
|
+
bpText += `\n\n📄 ページ: ${bpPage} / ${bpTotalPages}`;
|
|
4582
|
+
if (bpPage < bpTotalPages) {
|
|
4583
|
+
bpText += ` (次: page=${bpPage + 1})`;
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return { content: [{ type: "text", text: bpText }] };
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
case "get_blog_parts": {
|
|
4590
|
+
let client;
|
|
4591
|
+
try {
|
|
4592
|
+
client = registry.get(args?.site);
|
|
4593
|
+
} catch (e) {
|
|
4594
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
const bpPartsId = args?.partsId;
|
|
4598
|
+
if (!bpPartsId) {
|
|
4599
|
+
return { content: [{ type: "text", text: "❌ partsId は必須です" }], isError: true };
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
const siteName = args?.site || registry.defaultName();
|
|
4603
|
+
const { full: bpFull, contains: bpContains, section: bpSection, blockType: bpBlockType } = (args || {});
|
|
4604
|
+
|
|
4605
|
+
let bpState;
|
|
4606
|
+
try {
|
|
4607
|
+
bpState = await client.headlessGetStructure(bpPartsId);
|
|
4608
|
+
if (bpFull || bpContains || bpSection || bpBlockType) {
|
|
4609
|
+
const blocksData = await client.headlessGetBlocks(bpPartsId);
|
|
4610
|
+
bpState.allBlocks = blocksData.blocks;
|
|
4611
|
+
}
|
|
4612
|
+
} catch (e) {
|
|
4613
|
+
const formatted = formatHeadlessConflictError(e);
|
|
4614
|
+
if (formatted) return formatted;
|
|
4615
|
+
throw e;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// snapshot 作成(編集用 ref を付与)
|
|
4619
|
+
let _bpRefTag = (_idx) => '';
|
|
4620
|
+
let _bpSnapshotLine = '';
|
|
4621
|
+
const bpBlocks = bpState.allBlocks || [];
|
|
4622
|
+
if (bpBlocks.length > 0) {
|
|
4623
|
+
const _bpSnapshotId = generateSnapshotId();
|
|
4624
|
+
const _bpRevision = `rev_${Date.now().toString(36)}`;
|
|
4625
|
+
const _bpSnapshotBlocks = bpBlocks.map((b, i) => ({ ...b, ref: `r${i}` }));
|
|
4626
|
+
snapshotCache.set({
|
|
4627
|
+
snapshotId: _bpSnapshotId, postId: bpPartsId, mode: 'headless',
|
|
4628
|
+
sessionId: null, siteName,
|
|
4629
|
+
createdAt: Date.now(), revision: _bpRevision,
|
|
4630
|
+
blocks: _bpSnapshotBlocks, displayMode: bpFull ? 'full' : 'default',
|
|
4631
|
+
});
|
|
4632
|
+
const _bpRefMap = new Map(_bpSnapshotBlocks.map(b => [b.index, b.ref]));
|
|
4633
|
+
_bpRefTag = (idx) => { const r = _bpRefMap.get(idx); return r ? `|${r}` : ''; };
|
|
4634
|
+
_bpSnapshotLine = `\n[snapshot:${_bpSnapshotId} rev:${_bpRevision}]`;
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
if ((bpFull || bpContains || bpSection || bpBlockType) && bpBlocks.length > 0) {
|
|
4638
|
+
let filteredBlocks = bpBlocks;
|
|
4639
|
+
if (bpBlockType) filteredBlocks = filteredBlocks.filter(b => b.type === bpBlockType);
|
|
4640
|
+
if (bpContains) filteredBlocks = filteredBlocks.filter(b => (b.html || '').includes(bpContains));
|
|
4641
|
+
|
|
4642
|
+
const bpBlockList = filteredBlocks.map(b => {
|
|
4643
|
+
const indent = ' '.repeat((b.depth || 0) + 1);
|
|
4644
|
+
const hasChildren = b.isContainer ? ` ▼ [${b.childCount}子]` : '';
|
|
4645
|
+
return `${indent}[${b.index}${_bpRefTag(b.index)}] ${b.type}${hasChildren}`;
|
|
4646
|
+
}).join('\n');
|
|
4647
|
+
|
|
4648
|
+
return { content: [{ type: "text", text: `📦 ブログパーツ [${bpPartsId}]\n\n全ブロック (${filteredBlocks.length}件):\n\n${bpBlockList}${_bpSnapshotLine}\n[DEBUG] mode=headless` }] };
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// デフォルト: 見出し + サマリー
|
|
4652
|
+
const bpHeadings = (bpState.headings || [])
|
|
4653
|
+
.map(h => ` ${"#".repeat(h.level)} ${h.text} (index:${h.index})`)
|
|
4654
|
+
.join("\n");
|
|
4655
|
+
const bpSummaryLines = Object.entries(bpState.blockSummary || {})
|
|
4656
|
+
.map(([type, data]) => ` ${type}: ${data.count}個`)
|
|
4657
|
+
.join("\n");
|
|
4658
|
+
|
|
4659
|
+
let bpOutText = `📦 ブログパーツ [${bpPartsId}]\n\n`;
|
|
4660
|
+
if (bpHeadings) bpOutText += `見出し構造:\n${bpHeadings}\n\n`;
|
|
4661
|
+
bpOutText += `ブロック概要:\n${bpSummaryLines}`;
|
|
4662
|
+
bpOutText += _bpSnapshotLine;
|
|
4663
|
+
bpOutText += '\n[DEBUG] mode=headless';
|
|
4664
|
+
|
|
4665
|
+
return { content: [{ type: "text", text: bpOutText }] };
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
case "update_blog_parts": {
|
|
4669
|
+
let client;
|
|
4670
|
+
try {
|
|
4671
|
+
client = registry.get(args?.site);
|
|
4672
|
+
} catch (e) {
|
|
4673
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
const ubpPartsId = args?.partsId;
|
|
4677
|
+
if (!ubpPartsId) {
|
|
4678
|
+
return { content: [{ type: "text", text: "❌ partsId は必須です" }], isError: true };
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
const siteName = args?.site || registry.defaultName();
|
|
4682
|
+
|
|
4683
|
+
// ref/refs → index/indices 解決
|
|
4684
|
+
const ubpParams = { ...args };
|
|
4685
|
+
delete ubpParams.partsId;
|
|
4686
|
+
delete ubpParams.site;
|
|
4687
|
+
|
|
4688
|
+
if (ubpParams.snapshotId && (ubpParams.ref || ubpParams.refs)) {
|
|
4689
|
+
const snap = snapshotCache.get(ubpParams.snapshotId);
|
|
4690
|
+
if (!snap) {
|
|
4691
|
+
return { content: [{ type: "text", text: "❌ snapshotId が無効または期限切れです。get_blog_parts で再取得してください。" }], isError: true };
|
|
4692
|
+
}
|
|
4693
|
+
if (snap.postId !== ubpPartsId) {
|
|
4694
|
+
return { content: [{ type: "text", text: `❌ snapshot の postId (${snap.postId}) と partsId (${ubpPartsId}) が一致しません。` }], isError: true };
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
if (ubpParams.ref) {
|
|
4698
|
+
const refBlock = snap.blocks.find(b => b.ref === ubpParams.ref);
|
|
4699
|
+
if (!refBlock) {
|
|
4700
|
+
return { content: [{ type: "text", text: `❌ ref "${ubpParams.ref}" が snapshot 内に見つかりません。` }], isError: true };
|
|
4701
|
+
}
|
|
4702
|
+
ubpParams.index = refBlock.index;
|
|
4703
|
+
delete ubpParams.ref;
|
|
4704
|
+
}
|
|
4705
|
+
if (ubpParams.refs) {
|
|
4706
|
+
const indices = [];
|
|
4707
|
+
for (const r of ubpParams.refs) {
|
|
4708
|
+
const refBlock = snap.blocks.find(b => b.ref === r);
|
|
4709
|
+
if (!refBlock) {
|
|
4710
|
+
return { content: [{ type: "text", text: `❌ ref "${r}" が snapshot 内に見つかり���せん。` }], isError: true };
|
|
4711
|
+
}
|
|
4712
|
+
indices.push(refBlock.index);
|
|
4713
|
+
}
|
|
4714
|
+
ubpParams.indices = indices;
|
|
4715
|
+
delete ubpParams.refs;
|
|
4716
|
+
}
|
|
4717
|
+
delete ubpParams.snapshotId;
|
|
4718
|
+
}
|
|
4719
|
+
delete ubpParams.expectedRevision;
|
|
4720
|
+
|
|
4721
|
+
try {
|
|
4722
|
+
const ubpResult = await client.headlessUpdate(ubpPartsId, ubpParams);
|
|
4723
|
+
|
|
4724
|
+
// 新 snapshot 生成
|
|
4725
|
+
let ubpSnapshotLine = '';
|
|
4726
|
+
if (ubpResult.blocks && ubpResult.blocks.length > 0) {
|
|
4727
|
+
const _ubpSnapId = generateSnapshotId();
|
|
4728
|
+
const _ubpRev = `rev_${Date.now().toString(36)}`;
|
|
4729
|
+
const _ubpSnapBlocks = ubpResult.blocks.map((b, i) => ({ ...b, ref: `r${i}` }));
|
|
4730
|
+
snapshotCache.set({
|
|
4731
|
+
snapshotId: _ubpSnapId, postId: ubpPartsId, mode: 'headless',
|
|
4732
|
+
sessionId: null, siteName,
|
|
4733
|
+
createdAt: Date.now(), revision: _ubpRev,
|
|
4734
|
+
blocks: _ubpSnapBlocks, displayMode: 'full',
|
|
4735
|
+
});
|
|
4736
|
+
ubpSnapshotLine = `\n[snapshot:${_ubpSnapId} rev:${_ubpRev}]`;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
return { content: [{ type: "text", text: `✅ ブログパーツ [${ubpPartsId}] を更新しました。${ubpSnapshotLine}` }] };
|
|
4740
|
+
} catch (e) {
|
|
4741
|
+
const formatted = formatHeadlessConflictError(e);
|
|
4742
|
+
if (formatted) return formatted;
|
|
4743
|
+
throw e;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
|
|
3862
4747
|
case "list_taxonomies": {
|
|
3863
4748
|
let client;
|
|
3864
4749
|
try {
|
|
@@ -3897,6 +4782,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3897
4782
|
return { content: [{ type: "text", text }] };
|
|
3898
4783
|
}
|
|
3899
4784
|
|
|
4785
|
+
case "insert_blog_parts": {
|
|
4786
|
+
if (!args?.partsId) {
|
|
4787
|
+
return { content: [{ type: "text", text: "❌ partsId は必須です。" }], isError: true };
|
|
4788
|
+
}
|
|
4789
|
+
args.rawHTML = `<!-- wp:loos/blog-parts {"partsID":"${args.partsId}"} /-->`;
|
|
4790
|
+
// insert_block に fallthrough
|
|
4791
|
+
}
|
|
4792
|
+
|
|
3900
4793
|
case "insert_block": {
|
|
3901
4794
|
let { rawHTML, filePath } = (args || {});
|
|
3902
4795
|
|
|
@@ -3912,7 +4805,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3912
4805
|
|
|
3913
4806
|
// filePath → rawHTML 解決
|
|
3914
4807
|
if (filePath) {
|
|
3915
|
-
try { rawHTML =
|
|
4808
|
+
try { rawHTML = await resolveFileToBlockHTML(filePath); }
|
|
3916
4809
|
catch (e) { return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true }; }
|
|
3917
4810
|
}
|
|
3918
4811
|
if (!rawHTML) {
|
|
@@ -4025,10 +4918,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4025
4918
|
blocks = result.blocks || [];
|
|
4026
4919
|
}
|
|
4027
4920
|
|
|
4921
|
+
// エディタモード時、ブログパーツの展開を補完
|
|
4922
|
+
if (mode === 'editor' && client) {
|
|
4923
|
+
for (const b of blocks) {
|
|
4924
|
+
if (b.type === 'loos/blog-parts' && !b.blogParts) {
|
|
4925
|
+
const pid = parseInt(b.html?.match(/partsID["\s:]+["']?(\d+)/)?.[1], 10);
|
|
4926
|
+
if (pid > 0) {
|
|
4927
|
+
try {
|
|
4928
|
+
const bpResult = await client.headlessGetBlockHtml(pid, {});
|
|
4929
|
+
b.blogParts = {
|
|
4930
|
+
partsId: pid,
|
|
4931
|
+
blocks: bpResult?.blocks || [],
|
|
4932
|
+
};
|
|
4933
|
+
} catch (_) {}
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4028
4939
|
const _modeTag = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
4029
4940
|
let text = `📦 ブロックHTML取得 (${blocks.length}件)\n`;
|
|
4030
4941
|
for (const b of blocks) {
|
|
4031
|
-
text += `\n[${b.index}] ${b.type}\n
|
|
4942
|
+
text += `\n[${b.index}] ${b.type}\n`;
|
|
4943
|
+
if (b.blogParts && b.blogParts.blocks) {
|
|
4944
|
+
for (const inner of b.blogParts.blocks) {
|
|
4945
|
+
if ((inner.depth || 0) > 0) continue;
|
|
4946
|
+
text += `${inner.html}\n`;
|
|
4947
|
+
}
|
|
4948
|
+
} else {
|
|
4949
|
+
text += `${b.html}\n`;
|
|
4950
|
+
}
|
|
4032
4951
|
}
|
|
4033
4952
|
text += _modeTag;
|
|
4034
4953
|
return { content: [{ type: "text", text }] };
|
|
@@ -4289,8 +5208,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4289
5208
|
|
|
4290
5209
|
let url;
|
|
4291
5210
|
if (target === 'front') {
|
|
4292
|
-
|
|
4293
|
-
|
|
5211
|
+
try {
|
|
5212
|
+
const meta = await client.headlessGetMeta(postId);
|
|
5213
|
+
url = meta.link;
|
|
5214
|
+
} catch {
|
|
5215
|
+
try {
|
|
5216
|
+
const postData = await client.wpCoreRequest(`/wp/v2/posts/${postId}?_fields=link`);
|
|
5217
|
+
url = postData.link;
|
|
5218
|
+
} catch {
|
|
5219
|
+
const pageData = await client.wpCoreRequest(`/wp/v2/pages/${postId}?_fields=link`);
|
|
5220
|
+
url = pageData.link;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
4294
5223
|
} else {
|
|
4295
5224
|
url = `${client.wpUrl.replace(/\/$/, '')}/wp-admin/post.php?post=${postId}&action=edit`;
|
|
4296
5225
|
}
|
|
@@ -4335,6 +5264,178 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4335
5264
|
}
|
|
4336
5265
|
}
|
|
4337
5266
|
|
|
5267
|
+
case "upload_media": {
|
|
5268
|
+
const siteName = args?.site || 'default';
|
|
5269
|
+
let client;
|
|
5270
|
+
try { client = registry.get(siteName); }
|
|
5271
|
+
catch (e) { return errorResponse("upload_media", e.message, siteName); }
|
|
5272
|
+
|
|
5273
|
+
if (!args?.filePath) return errorResponse("upload_media", "filePath is required", siteName);
|
|
5274
|
+
if (!args?.name) return errorResponse("upload_media", "name is required", siteName);
|
|
5275
|
+
if (!args?.alt) return errorResponse("upload_media", "alt is required", siteName);
|
|
5276
|
+
|
|
5277
|
+
let fileInfo;
|
|
5278
|
+
try { fileInfo = validateImageFile(args.filePath); }
|
|
5279
|
+
catch (e) { return errorResponse("upload_media", e.message, siteName); }
|
|
5280
|
+
|
|
5281
|
+
const uploadFilename = `${args.name}${fileInfo.ext}`;
|
|
5282
|
+
const fileBuffer = readFileSync(fileInfo.resolved);
|
|
5283
|
+
|
|
5284
|
+
let media;
|
|
5285
|
+
try {
|
|
5286
|
+
media = await client.uploadMedia(fileBuffer, uploadFilename, fileInfo.contentType);
|
|
5287
|
+
} catch (e) {
|
|
5288
|
+
return errorResponse("upload_media", `Upload failed: ${e.message}`, siteName);
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
const titleText = args?.title || args.alt;
|
|
5292
|
+
try {
|
|
5293
|
+
await client.updateMediaMeta(media.id, { title: titleText, alt_text: args.alt });
|
|
5294
|
+
} catch (e) {
|
|
5295
|
+
// meta 更新失敗は警告のみ(アップロード自体は成功)
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
const url = media.source_url || '';
|
|
5299
|
+
const width = media.media_details?.width || null;
|
|
5300
|
+
const height = media.media_details?.height || null;
|
|
5301
|
+
const text = `✅ Uploaded: ID=${media.id}\n` +
|
|
5302
|
+
` Title: ${titleText}\n` +
|
|
5303
|
+
` Alt: ${args.alt}\n` +
|
|
5304
|
+
` URL: ${url}\n` +
|
|
5305
|
+
(width && height ? ` Size: ${width}x${height}\n` : '') +
|
|
5306
|
+
` Filename: ${uploadFilename}`;
|
|
5307
|
+
return { content: [{ type: "text", text }] };
|
|
5308
|
+
}
|
|
5309
|
+
|
|
5310
|
+
case "search_media": {
|
|
5311
|
+
const siteName = args?.site || 'default';
|
|
5312
|
+
let client;
|
|
5313
|
+
try { client = registry.get(siteName); }
|
|
5314
|
+
catch (e) { return errorResponse("search_media", e.message, siteName); }
|
|
5315
|
+
|
|
5316
|
+
if (!args?.query && !args?.folder) return errorResponse("search_media", "query または folder のどちらかは必須です", siteName);
|
|
5317
|
+
|
|
5318
|
+
const perPage = args?.per_page || 10;
|
|
5319
|
+
|
|
5320
|
+
// --- FileBird folder filtering ---
|
|
5321
|
+
let folderIncludeIds = null;
|
|
5322
|
+
let folderInfo = '';
|
|
5323
|
+
if (args?.folder) {
|
|
5324
|
+
if (!client.filebirdToken) {
|
|
5325
|
+
return errorResponse("search_media", "FileBird APIキーが未設定です。connections.json に filebirdToken を追加してください", siteName);
|
|
5326
|
+
}
|
|
5327
|
+
const folders = await client.getFileBirdFolders();
|
|
5328
|
+
if (!folders) {
|
|
5329
|
+
return errorResponse("search_media", "FileBird API へのアクセスに失敗しました", siteName);
|
|
5330
|
+
}
|
|
5331
|
+
const flat = flattenFileBirdFolders(folders);
|
|
5332
|
+
const matched = flat.filter(f => f.name.includes(args.folder));
|
|
5333
|
+
if (matched.length === 0) {
|
|
5334
|
+
return errorResponse("search_media", `フォルダ "${args.folder}" が見つかりません。\n利用可能: ${flat.map(f => f.name).join(', ')}`, siteName);
|
|
5335
|
+
}
|
|
5336
|
+
if (matched.length > 1) {
|
|
5337
|
+
let text = `📁 "${args.folder}" に複数のフォルダがマッチしました。絞り込んでください:\n\n`;
|
|
5338
|
+
for (const f of matched) {
|
|
5339
|
+
text += ` [ID: ${f.id}] ${f.name} (${f.count}件)\n`;
|
|
5340
|
+
}
|
|
5341
|
+
return { content: [{ type: "text", text }] };
|
|
5342
|
+
}
|
|
5343
|
+
const folder = matched[0];
|
|
5344
|
+
const attachmentIds = await client.getFileBirdAttachmentIds(folder.id);
|
|
5345
|
+
if (!attachmentIds || attachmentIds.length === 0) {
|
|
5346
|
+
return { content: [{ type: "text", text: `📁 ${folder.name} (0件)\n\n❌ フォルダ内にメディアがありません。` }] };
|
|
5347
|
+
}
|
|
5348
|
+
folderIncludeIds = attachmentIds;
|
|
5349
|
+
const totalCount = attachmentIds.length;
|
|
5350
|
+
const limited = totalCount > 100;
|
|
5351
|
+
folderInfo = `📁 Folder: ${folder.name} (${totalCount}件${limited ? '、先頭100件から検索' : ''})\n`;
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
// --- keyword search (with or without folder filter) ---
|
|
5355
|
+
if (args?.query) {
|
|
5356
|
+
const variants = generateSearchVariants(args.query, args?.strip, args?.prefix);
|
|
5357
|
+
const results = await Promise.allSettled(
|
|
5358
|
+
variants.map(v => client.searchMedia(v, perPage, folderIncludeIds))
|
|
5359
|
+
);
|
|
5360
|
+
|
|
5361
|
+
const seenIds = new Set();
|
|
5362
|
+
const hits = [];
|
|
5363
|
+
for (let i = 0; i < results.length; i++) {
|
|
5364
|
+
if (results[i].status !== 'fulfilled') continue;
|
|
5365
|
+
for (const item of results[i].value) {
|
|
5366
|
+
if (seenIds.has(item.id)) continue;
|
|
5367
|
+
seenIds.add(item.id);
|
|
5368
|
+
hits.push({
|
|
5369
|
+
id: item.id,
|
|
5370
|
+
title: item.title?.rendered || '',
|
|
5371
|
+
alt: item.alt_text || '',
|
|
5372
|
+
url: item.source_url || '',
|
|
5373
|
+
width: item.media_details?.width || null,
|
|
5374
|
+
height: item.media_details?.height || null,
|
|
5375
|
+
matched: variants[i],
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
let text = folderInfo;
|
|
5381
|
+
text += `🔍 Search: "${args.query}"\n`;
|
|
5382
|
+
text += `🔄 Variants (${variants.length}): ${variants.join(', ')}\n\n`;
|
|
5383
|
+
|
|
5384
|
+
if (hits.length === 0) {
|
|
5385
|
+
text += '❌ No results found.';
|
|
5386
|
+
} else {
|
|
5387
|
+
text += `📷 ${hits.length} result(s):\n\n`;
|
|
5388
|
+
for (const h of hits) {
|
|
5389
|
+
text += ` [ID: ${h.id}] ${h.title}\n`;
|
|
5390
|
+
text += ` Alt: ${h.alt}\n`;
|
|
5391
|
+
text += ` URL: ${h.url}\n`;
|
|
5392
|
+
if (h.width && h.height) text += ` Size: ${h.width}x${h.height}\n`;
|
|
5393
|
+
text += ` Matched: "${h.matched}"\n\n`;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
return { content: [{ type: "text", text }] };
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
// --- folder only (no query) ---
|
|
5400
|
+
const mediaItems = await client.searchMedia(null, perPage, folderIncludeIds);
|
|
5401
|
+
let text = folderInfo + '\n';
|
|
5402
|
+
if (!mediaItems || mediaItems.length === 0) {
|
|
5403
|
+
text += '❌ No results found.';
|
|
5404
|
+
} else {
|
|
5405
|
+
text += `📷 ${mediaItems.length} result(s):\n\n`;
|
|
5406
|
+
for (const item of mediaItems) {
|
|
5407
|
+
text += ` [ID: ${item.id}] ${item.title?.rendered || ''}\n`;
|
|
5408
|
+
text += ` Alt: ${item.alt_text || ''}\n`;
|
|
5409
|
+
text += ` URL: ${item.source_url || ''}\n`;
|
|
5410
|
+
const w = item.media_details?.width, h = item.media_details?.height;
|
|
5411
|
+
if (w && h) text += ` Size: ${w}x${h}\n`;
|
|
5412
|
+
text += '\n';
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
return { content: [{ type: "text", text }] };
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5418
|
+
case "list_media_folders": {
|
|
5419
|
+
const siteName = args?.site || 'default';
|
|
5420
|
+
let client;
|
|
5421
|
+
try { client = registry.get(siteName); }
|
|
5422
|
+
catch (e) { return errorResponse("list_media_folders", e.message, siteName); }
|
|
5423
|
+
|
|
5424
|
+
if (!client.filebirdToken) {
|
|
5425
|
+
return { content: [{ type: "text", text: "⚠️ FileBird 未導入または APIキーが未設定です。\nconnections.json に filebirdToken を追加してください。" }] };
|
|
5426
|
+
}
|
|
5427
|
+
const folders = await client.getFileBirdFolders();
|
|
5428
|
+
if (!folders) {
|
|
5429
|
+
return errorResponse("list_media_folders", "FileBird API へのアクセスに失敗しました", siteName);
|
|
5430
|
+
}
|
|
5431
|
+
const flat = flattenFileBirdFolders(folders);
|
|
5432
|
+
let text = `📁 FileBird フォルダ一覧 (${flat.length}件)\n\n`;
|
|
5433
|
+
for (const f of flat) {
|
|
5434
|
+
text += ` [ID: ${f.id}] ${f.name} (${f.count}件)\n`;
|
|
5435
|
+
}
|
|
5436
|
+
return { content: [{ type: "text", text }] };
|
|
5437
|
+
}
|
|
5438
|
+
|
|
4338
5439
|
case "list_connections": {
|
|
4339
5440
|
const conns = registry.list();
|
|
4340
5441
|
const text = conns.map(c => {
|
|
@@ -4375,6 +5476,103 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4375
5476
|
return { content: [{ type: "text", text: `✅ フィードバックを送信しました` }] };
|
|
4376
5477
|
}
|
|
4377
5478
|
|
|
5479
|
+
case "search_asp_link": {
|
|
5480
|
+
let client;
|
|
5481
|
+
try {
|
|
5482
|
+
client = registry.get(args?.site);
|
|
5483
|
+
} catch (e) {
|
|
5484
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
const slug = args?.slug?.trim() || undefined;
|
|
5488
|
+
const search = args?.search?.trim() || undefined;
|
|
5489
|
+
|
|
5490
|
+
if (slug && search) {
|
|
5491
|
+
return { content: [{ type: "text", text: "❌ slug と search は同時に指定できません。どちらか一方を使用してください。" }], isError: true };
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5494
|
+
let result;
|
|
5495
|
+
try {
|
|
5496
|
+
result = await client.searchAspLinks({
|
|
5497
|
+
search,
|
|
5498
|
+
slug,
|
|
5499
|
+
per_page: args?.per_page,
|
|
5500
|
+
page: args?.page,
|
|
5501
|
+
});
|
|
5502
|
+
} catch (e) {
|
|
5503
|
+
return { content: [{ type: "text", text: `❌ ASPリンク検索エラー: ${e.message}` }], isError: true };
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
const items = result.items || [];
|
|
5507
|
+
if (items.length === 0) {
|
|
5508
|
+
return { content: [{ type: "text", text: "🔗 該当するASPリンクはありません。" }] };
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
const list = items.map(item => {
|
|
5512
|
+
const lines = [
|
|
5513
|
+
` [${item.id}] ${item.title}`,
|
|
5514
|
+
` 案件ID: ${item.data_id} | slug: ${item.slug} | permalink: ${item.permalink}`,
|
|
5515
|
+
];
|
|
5516
|
+
if (item.asp_url) lines.push(` ASP URL: ${item.asp_url}`);
|
|
5517
|
+
if (item.asp_url_sp) lines.push(` ASP URL(B): ${item.asp_url_sp}`);
|
|
5518
|
+
if (item.direct_url) lines.push(` 直リンク: ${item.direct_url}`);
|
|
5519
|
+
lines.push(` tag: ${item.tag}`);
|
|
5520
|
+
return lines.join('\n');
|
|
5521
|
+
}).join('\n\n');
|
|
5522
|
+
|
|
5523
|
+
let text = `🔗 ASPリンク検索結果 (${items.length}件 / 全${result.total}件)\n\n${list}`;
|
|
5524
|
+
if (result.total_pages > 1) {
|
|
5525
|
+
text += `\n\n📄 ページ: ${result.page} / ${result.total_pages}`;
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5528
|
+
return { content: [{ type: "text", text }] };
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
case "register_asp_link": {
|
|
5532
|
+
let client;
|
|
5533
|
+
try {
|
|
5534
|
+
client = registry.get(args?.site);
|
|
5535
|
+
} catch (e) {
|
|
5536
|
+
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
if (!args?.title || !args?.slug || !args?.asp_url) {
|
|
5540
|
+
return { content: [{ type: "text", text: "❌ title, slug, asp_url は必須です。" }], isError: true };
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
let result;
|
|
5544
|
+
try {
|
|
5545
|
+
result = await client.createAspLink({
|
|
5546
|
+
title: args.title,
|
|
5547
|
+
slug: args.slug,
|
|
5548
|
+
asp_id: args?.asp_id,
|
|
5549
|
+
asp_name: args?.asp_name,
|
|
5550
|
+
asp_url: args.asp_url,
|
|
5551
|
+
asp_url_sp: args?.asp_url_sp,
|
|
5552
|
+
direct_url: args?.direct_url,
|
|
5553
|
+
permalink: args?.permalink,
|
|
5554
|
+
});
|
|
5555
|
+
} catch (e) {
|
|
5556
|
+
return { content: [{ type: "text", text: `❌ ASPリンク登録エラー: ${e.message}` }], isError: true };
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
const lines = [
|
|
5560
|
+
`✅ ASPリンク登録完了`,
|
|
5561
|
+
``,
|
|
5562
|
+
` ID: ${result.id}`,
|
|
5563
|
+
` タイトル: ${result.title}`,
|
|
5564
|
+
` slug: ${result.slug}`,
|
|
5565
|
+
` permalink: ${result.permalink}`,
|
|
5566
|
+
` 案件ID: ${result.data_id}`,
|
|
5567
|
+
];
|
|
5568
|
+
if (result.asp_url) lines.push(` ASP URL: ${result.asp_url}`);
|
|
5569
|
+
if (result.asp_url_sp) lines.push(` ASP URL(B): ${result.asp_url_sp}`);
|
|
5570
|
+
if (result.direct_url) lines.push(` 直リンク: ${result.direct_url}`);
|
|
5571
|
+
lines.push(` tag: ${result.tag}`);
|
|
5572
|
+
|
|
5573
|
+
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
5574
|
+
}
|
|
5575
|
+
|
|
4378
5576
|
default:
|
|
4379
5577
|
throw new Error(`Unknown tool: ${name}`);
|
|
4380
5578
|
} })();
|