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.
@@ -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 { data: posts } = await client.listPosts({ slug, status: 'any' });
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 = readHTMLFromFile(args.filePath).html;
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
- return `${t.indent}[${b.index}${_refTag(b.index)}] ${t.marker}${b.type}${t.childInfo} - ${b.section || "(記事冒頭)"} ${t.depthInfo}\n${t.indent} ${attrPreview}${attrPreview.length >= 200 ? '...' : ''}`;
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 text = [
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
- `カテゴリ: ${JSON.stringify(m.categories)}`,
3764
- `タグ: ${JSON.stringify(m.tags)}`,
3765
- `アイキャッチ: ${m.featuredImage || 'なし'}`,
3766
- `抜粋: ${m.excerpt || 'なし'}`,
3767
- `更新日: ${m.modified}`,
3768
- ].join('\n');
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
- // listArgs を構築(slug 指定時: status 未指定なら 'any' に自動設定)
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 { data: posts, total, totalPages } = await client.listPosts(listArgs);
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 (!posts || posts.length === 0) {
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
- return ` [${p.id}] ${p.title?.rendered || '(無題)'}\n` +
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 = `📋 投稿一覧 (${posts.length}件 / 全${total}件)\n\n${list}`;
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 = readHTMLFromFile(filePath).html; }
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${b.html}\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
- const postData = await client.wpCoreRequest(`/wp/v2/posts/${postId}?_fields=link`);
4293
- url = postData.link;
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
  } })();