friday-mcp-v2 3.1.0 → 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 +329 -64
- package/dist/wordpress-api.js +32 -6
- package/package.json +3 -2
package/dist/mcp-server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { readFileSync, statSync, realpathSync, appendFileSync, mkdirSync, exists
|
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
17
17
|
import { resolve as resolvePath, sep, dirname, join as joinPath, extname } from "node:path";
|
|
18
18
|
import { marked } from "marked";
|
|
19
|
+
import { parse as parseHTML } from "node-html-parser";
|
|
19
20
|
import os from "node:os";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
// package.json からバージョンを取得
|
|
@@ -457,6 +458,117 @@ function readHTMLFromFile(filePath, maxSizeBytes = 2 * 1024 * 1024) {
|
|
|
457
458
|
return { html };
|
|
458
459
|
}
|
|
459
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
|
+
|
|
460
572
|
// ========================================
|
|
461
573
|
// Media Utilities
|
|
462
574
|
// ========================================
|
|
@@ -610,6 +722,16 @@ function generateSearchVariants(query, strip = [], prefixes = []) {
|
|
|
610
722
|
return [...variants];
|
|
611
723
|
}
|
|
612
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
|
+
|
|
613
735
|
/**
|
|
614
736
|
* 単一クライアントの Editor/Headless 判定
|
|
615
737
|
* @returns {{ mode: 'editor'|'headless'|'error', postId?: number, message?: string, client?: FridayWPClient }}
|
|
@@ -2381,13 +2503,14 @@ const tools = [
|
|
|
2381
2503
|
},
|
|
2382
2504
|
{
|
|
2383
2505
|
name: "list_blog_parts",
|
|
2384
|
-
description: "List or search blog parts (reusable blocks). SWELL theme only.",
|
|
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.",
|
|
2385
2507
|
inputSchema: {
|
|
2386
2508
|
type: "object",
|
|
2387
2509
|
properties: {
|
|
2388
2510
|
site: siteParam,
|
|
2389
|
-
|
|
2390
|
-
|
|
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)" },
|
|
2391
2514
|
per_page: { type: "integer", description: "Per page (1-100)", minimum: 1, maximum: 100 },
|
|
2392
2515
|
page: { type: "integer", description: "Page number", minimum: 1 },
|
|
2393
2516
|
},
|
|
@@ -2411,12 +2534,12 @@ const tools = [
|
|
|
2411
2534
|
},
|
|
2412
2535
|
{
|
|
2413
2536
|
name: "create_blog_parts",
|
|
2414
|
-
description: "Create a new blog parts (reusable block). SWELL theme only. Returns
|
|
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.",
|
|
2415
2538
|
inputSchema: {
|
|
2416
2539
|
type: "object",
|
|
2417
2540
|
properties: {
|
|
2418
2541
|
title: { type: "string", description: "Blog parts title" },
|
|
2419
|
-
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Status (default:
|
|
2542
|
+
status: { type: "string", enum: ["publish", "draft", "pending", "private"], description: "Status (default: publish)" },
|
|
2420
2543
|
slug: { type: "string", description: "URL slug" },
|
|
2421
2544
|
content: { type: "string", description: "HTML content (exclusive with filePath)" },
|
|
2422
2545
|
filePath: { type: "string", description: "Local file path (.md or .html, exclusive with content)" },
|
|
@@ -2456,6 +2579,23 @@ const tools = [
|
|
|
2456
2579
|
required: ["partsId"],
|
|
2457
2580
|
},
|
|
2458
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
|
+
},
|
|
2459
2599
|
{
|
|
2460
2600
|
name: "list_taxonomies",
|
|
2461
2601
|
description: "List categories or tags.",
|
|
@@ -2504,7 +2644,7 @@ const tools = [
|
|
|
2504
2644
|
},
|
|
2505
2645
|
{
|
|
2506
2646
|
name: "get_block_html",
|
|
2507
|
-
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.",
|
|
2508
2648
|
inputSchema: {
|
|
2509
2649
|
type: "object",
|
|
2510
2650
|
properties: {
|
|
@@ -2701,11 +2841,21 @@ const tools = [
|
|
|
2701
2841
|
properties: {
|
|
2702
2842
|
site: siteParam,
|
|
2703
2843
|
query: { type: "string", description: "Search keyword" },
|
|
2844
|
+
folder: { type: "string", description: "FileBird folder name (partial match). Filters media to this folder." },
|
|
2704
2845
|
strip: { type: "array", items: { type: "string" }, description: "Suffixes to strip (e.g. ['先生', 'さん'])" },
|
|
2705
2846
|
prefix: { type: "array", items: { type: "string" }, description: "Prefixes to add (e.g. ['エキサイト-'])" },
|
|
2706
2847
|
per_page: { type: "integer", description: "Results per variant (1-20, default: 10)", minimum: 1, maximum: 20 },
|
|
2707
2848
|
},
|
|
2708
|
-
|
|
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
|
+
},
|
|
2709
2859
|
},
|
|
2710
2860
|
},
|
|
2711
2861
|
{
|
|
@@ -2767,6 +2917,7 @@ const tools = [
|
|
|
2767
2917
|
asp_url: { type: "string", description: "ASP affiliate URL (required)" },
|
|
2768
2918
|
asp_url_sp: { type: "string", description: "ASP URL (B) for A/B testing (optional)" },
|
|
2769
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)" },
|
|
2770
2921
|
},
|
|
2771
2922
|
required: ["title", "slug", "asp_url"],
|
|
2772
2923
|
},
|
|
@@ -2898,7 +3049,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
2898
3049
|
return { content: [{ type: "text", text: "❌ filePath は attributeUpdates と併用できません。" }], isError: true };
|
|
2899
3050
|
}
|
|
2900
3051
|
try {
|
|
2901
|
-
newHTML =
|
|
3052
|
+
newHTML = await resolveFileToBlockHTML(args.filePath);
|
|
2902
3053
|
} catch (e) {
|
|
2903
3054
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
2904
3055
|
}
|
|
@@ -3168,7 +3319,7 @@ async function handleUpdateBlocksTool(args, toolName) {
|
|
|
3168
3319
|
}
|
|
3169
3320
|
|
|
3170
3321
|
// ツール実行のハンドラ
|
|
3171
|
-
const _WRITE_TOOLS = new Set(['update_blocks', 'delete_block', 'move_block', 'duplicate_block', 'insert_block', 'table_operations', 'update_blog_parts']);
|
|
3322
|
+
const _WRITE_TOOLS = new Set(['update_blocks', 'delete_block', 'move_block', 'duplicate_block', 'insert_block', 'table_operations', 'update_blog_parts', 'insert_blog_parts']);
|
|
3172
3323
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3173
3324
|
const { name, arguments: args } = request.params;
|
|
3174
3325
|
const _toolLogStart = Date.now();
|
|
@@ -4230,15 +4381,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4230
4381
|
|
|
4231
4382
|
if (filePath) {
|
|
4232
4383
|
try {
|
|
4233
|
-
|
|
4234
|
-
const raw = readHTMLFromFile(filePath).html;
|
|
4235
|
-
if (ext === '.md') {
|
|
4236
|
-
content = await marked.parse(raw);
|
|
4237
|
-
} else if (ext === '.html' || ext === '.htm') {
|
|
4238
|
-
content = raw;
|
|
4239
|
-
} else {
|
|
4240
|
-
return { content: [{ type: "text", text: `❌ 対応していないファイル形式です: ${ext} (.md または .html のみ)` }], isError: true };
|
|
4241
|
-
}
|
|
4384
|
+
content = await resolveFileToBlockHTML(filePath);
|
|
4242
4385
|
} catch (e) {
|
|
4243
4386
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4244
4387
|
}
|
|
@@ -4360,15 +4503,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4360
4503
|
|
|
4361
4504
|
if (filePath) {
|
|
4362
4505
|
try {
|
|
4363
|
-
|
|
4364
|
-
const raw = readHTMLFromFile(filePath).html;
|
|
4365
|
-
if (ext === '.md') {
|
|
4366
|
-
content = await marked.parse(raw);
|
|
4367
|
-
} else if (ext === '.html' || ext === '.htm') {
|
|
4368
|
-
content = raw;
|
|
4369
|
-
} else {
|
|
4370
|
-
return { content: [{ type: "text", text: `❌ 対応していないファイル形式です: ${ext} (.md または .html のみ)` }], isError: true };
|
|
4371
|
-
}
|
|
4506
|
+
content = await resolveFileToBlockHTML(filePath);
|
|
4372
4507
|
} catch (e) {
|
|
4373
4508
|
return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true };
|
|
4374
4509
|
}
|
|
@@ -4376,7 +4511,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4376
4511
|
|
|
4377
4512
|
const postData = {
|
|
4378
4513
|
title,
|
|
4379
|
-
status: args?.status || '
|
|
4514
|
+
status: args?.status || 'publish',
|
|
4380
4515
|
post_type: 'blog_parts',
|
|
4381
4516
|
};
|
|
4382
4517
|
if (args?.slug !== undefined) postData.slug = args.slug;
|
|
@@ -4410,6 +4545,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4410
4545
|
}
|
|
4411
4546
|
|
|
4412
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
|
+
|
|
4413
4566
|
const bpParts = bpResult.items || [];
|
|
4414
4567
|
const bpTotal = bpResult.total || 0;
|
|
4415
4568
|
const bpTotalPages = bpResult.total_pages || 0;
|
|
@@ -4423,7 +4576,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4423
4576
|
return ` [${p.id}] ${p.title || '(無題)'}\n ステータス: ${p.status} | 更新: ${p.modified || '-'}`;
|
|
4424
4577
|
}).join('\n\n');
|
|
4425
4578
|
|
|
4426
|
-
let bpText = `📋
|
|
4579
|
+
let bpText = `📋 ブログパーツ一覧 (${bpParts.length}件 / 全${bpTotal}件)\n\n${bpList}`;
|
|
4427
4580
|
if (bpTotalPages > 1) {
|
|
4428
4581
|
bpText += `\n\n📄 ページ: ${bpPage} / ${bpTotalPages}`;
|
|
4429
4582
|
if (bpPage < bpTotalPages) {
|
|
@@ -4629,6 +4782,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4629
4782
|
return { content: [{ type: "text", text }] };
|
|
4630
4783
|
}
|
|
4631
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
|
+
|
|
4632
4793
|
case "insert_block": {
|
|
4633
4794
|
let { rawHTML, filePath } = (args || {});
|
|
4634
4795
|
|
|
@@ -4644,7 +4805,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4644
4805
|
|
|
4645
4806
|
// filePath → rawHTML 解決
|
|
4646
4807
|
if (filePath) {
|
|
4647
|
-
try { rawHTML =
|
|
4808
|
+
try { rawHTML = await resolveFileToBlockHTML(filePath); }
|
|
4648
4809
|
catch (e) { return { content: [{ type: "text", text: `❌ ${e.message}` }], isError: true }; }
|
|
4649
4810
|
}
|
|
4650
4811
|
if (!rawHTML) {
|
|
@@ -4757,10 +4918,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4757
4918
|
blocks = result.blocks || [];
|
|
4758
4919
|
}
|
|
4759
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
|
+
|
|
4760
4939
|
const _modeTag = process.env.FRIDAY_DEBUG === '1' ? `\n[DEBUG] mode=${mode}` : '';
|
|
4761
4940
|
let text = `📦 ブロックHTML取得 (${blocks.length}件)\n`;
|
|
4762
4941
|
for (const b of blocks) {
|
|
4763
|
-
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
|
+
}
|
|
4764
4951
|
}
|
|
4765
4952
|
text += _modeTag;
|
|
4766
4953
|
return { content: [{ type: "text", text }] };
|
|
@@ -5126,50 +5313,126 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5126
5313
|
try { client = registry.get(siteName); }
|
|
5127
5314
|
catch (e) { return errorResponse("search_media", e.message, siteName); }
|
|
5128
5315
|
|
|
5129
|
-
if (!args?.query) return errorResponse("search_media", "query
|
|
5316
|
+
if (!args?.query && !args?.folder) return errorResponse("search_media", "query または folder のどちらかは必須です", siteName);
|
|
5130
5317
|
|
|
5131
|
-
const variants = generateSearchVariants(args.query, args?.strip, args?.prefix);
|
|
5132
5318
|
const perPage = args?.per_page || 10;
|
|
5133
5319
|
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
seenIds.add(item.id);
|
|
5145
|
-
hits.push({
|
|
5146
|
-
id: item.id,
|
|
5147
|
-
title: item.title?.rendered || '',
|
|
5148
|
-
alt: item.alt_text || '',
|
|
5149
|
-
url: item.source_url || '',
|
|
5150
|
-
width: item.media_details?.width || null,
|
|
5151
|
-
height: item.media_details?.height || null,
|
|
5152
|
-
matched: variants[i],
|
|
5153
|
-
});
|
|
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);
|
|
5154
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`;
|
|
5155
5352
|
}
|
|
5156
5353
|
|
|
5157
|
-
|
|
5158
|
-
|
|
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
|
+
}
|
|
5159
5379
|
|
|
5160
|
-
|
|
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) {
|
|
5161
5403
|
text += '❌ No results found.';
|
|
5162
5404
|
} else {
|
|
5163
|
-
text += `📷 ${
|
|
5164
|
-
for (const
|
|
5165
|
-
text += ` [ID: ${
|
|
5166
|
-
text += ` Alt: ${
|
|
5167
|
-
text += ` URL: ${
|
|
5168
|
-
|
|
5169
|
-
text += `
|
|
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';
|
|
5170
5413
|
}
|
|
5171
5414
|
}
|
|
5415
|
+
return { content: [{ type: "text", text }] };
|
|
5416
|
+
}
|
|
5172
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
|
+
}
|
|
5173
5436
|
return { content: [{ type: "text", text }] };
|
|
5174
5437
|
}
|
|
5175
5438
|
|
|
@@ -5248,7 +5511,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5248
5511
|
const list = items.map(item => {
|
|
5249
5512
|
const lines = [
|
|
5250
5513
|
` [${item.id}] ${item.title}`,
|
|
5251
|
-
` 案件ID: ${item.data_id} | slug: ${item.slug}`,
|
|
5514
|
+
` 案件ID: ${item.data_id} | slug: ${item.slug} | permalink: ${item.permalink}`,
|
|
5252
5515
|
];
|
|
5253
5516
|
if (item.asp_url) lines.push(` ASP URL: ${item.asp_url}`);
|
|
5254
5517
|
if (item.asp_url_sp) lines.push(` ASP URL(B): ${item.asp_url_sp}`);
|
|
@@ -5287,6 +5550,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5287
5550
|
asp_url: args.asp_url,
|
|
5288
5551
|
asp_url_sp: args?.asp_url_sp,
|
|
5289
5552
|
direct_url: args?.direct_url,
|
|
5553
|
+
permalink: args?.permalink,
|
|
5290
5554
|
});
|
|
5291
5555
|
} catch (e) {
|
|
5292
5556
|
return { content: [{ type: "text", text: `❌ ASPリンク登録エラー: ${e.message}` }], isError: true };
|
|
@@ -5298,6 +5562,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5298
5562
|
` ID: ${result.id}`,
|
|
5299
5563
|
` タイトル: ${result.title}`,
|
|
5300
5564
|
` slug: ${result.slug}`,
|
|
5565
|
+
` permalink: ${result.permalink}`,
|
|
5301
5566
|
` 案件ID: ${result.data_id}`,
|
|
5302
5567
|
];
|
|
5303
5568
|
if (result.asp_url) lines.push(` ASP URL: ${result.asp_url}`);
|
package/dist/wordpress-api.js
CHANGED
|
@@ -279,9 +279,13 @@ export class FridayWPClient {
|
|
|
279
279
|
|
|
280
280
|
async searchBlogParts(params = {}) {
|
|
281
281
|
const query = new URLSearchParams();
|
|
282
|
-
if (params.
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
if (params.partsId) {
|
|
283
|
+
query.set('parts_id', String(params.partsId));
|
|
284
|
+
} else {
|
|
285
|
+
if (params.search) query.set('search', params.search);
|
|
286
|
+
if (params.slug) query.set('slug', params.slug);
|
|
287
|
+
if (params.status) query.set('status', params.status);
|
|
288
|
+
}
|
|
285
289
|
if (params.per_page) query.set('per_page', String(Math.min(Math.max(1, params.per_page), 100)));
|
|
286
290
|
if (params.page) query.set('page', String(Math.max(1, params.page)));
|
|
287
291
|
const qs = query.toString();
|
|
@@ -457,15 +461,36 @@ export class FridayWPClient {
|
|
|
457
461
|
* @param {number} perPage 取得件数(デフォルト10)
|
|
458
462
|
* @returns {object[]} メディアオブジェクト配列
|
|
459
463
|
*/
|
|
460
|
-
async searchMedia(query, perPage = 10) {
|
|
464
|
+
async searchMedia(query, perPage = 10, includeIds = null) {
|
|
461
465
|
const params = new URLSearchParams({
|
|
462
|
-
search: query,
|
|
463
466
|
media_type: 'image',
|
|
464
467
|
per_page: String(Math.min(Math.max(1, perPage), 20)),
|
|
465
468
|
});
|
|
469
|
+
if (query) params.set('search', query);
|
|
470
|
+
if (includeIds && includeIds.length > 0) {
|
|
471
|
+
params.set('include', includeIds.slice(0, 100).join(','));
|
|
472
|
+
}
|
|
466
473
|
return this.wpCoreRequest(`/wp/v2/media?${params.toString()}`);
|
|
467
474
|
}
|
|
468
475
|
|
|
476
|
+
async getFileBirdFolders() {
|
|
477
|
+
if (!this.filebirdToken) return null;
|
|
478
|
+
const url = `${this.wpUrl}/wp-json/filebird/public/v1/folders?token=${this.filebirdToken}`;
|
|
479
|
+
const res = await fetch(url);
|
|
480
|
+
if (!res.ok) return null;
|
|
481
|
+
const json = await res.json();
|
|
482
|
+
return json.success ? json.data.folders : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async getFileBirdAttachmentIds(folderId) {
|
|
486
|
+
if (!this.filebirdToken) return null;
|
|
487
|
+
const url = `${this.wpUrl}/wp-json/filebird/public/v1/attachment-id/?folder_id=${folderId}&token=${this.filebirdToken}`;
|
|
488
|
+
const res = await fetch(url);
|
|
489
|
+
if (!res.ok) return null;
|
|
490
|
+
const json = await res.json();
|
|
491
|
+
return json.success ? json.data.attachment_ids : null;
|
|
492
|
+
}
|
|
493
|
+
|
|
469
494
|
// ========================================
|
|
470
495
|
// ASP Link Management
|
|
471
496
|
// ========================================
|
|
@@ -504,6 +529,7 @@ export class FridayWPClient {
|
|
|
504
529
|
instance.wpAppPassword = config.pass;
|
|
505
530
|
instance.authHeader = "Basic " + Buffer.from(`${config.user}:${config.pass}`).toString("base64");
|
|
506
531
|
instance.baseUrl = config.url.replace(/\/$/, '') + '/wp-json/friday/v1';
|
|
532
|
+
instance.filebirdToken = config.filebirdToken || config.filebird_token || null;
|
|
507
533
|
instance.bridgeClient = null;
|
|
508
534
|
return instance;
|
|
509
535
|
}
|
|
@@ -587,7 +613,7 @@ export class ConnectionRegistry {
|
|
|
587
613
|
/** FRIDAY_CONN_{name}_{URL|USER|PASS} をパースして { name: { url, user, pass } } を返す */
|
|
588
614
|
_parseConnEnvVars() {
|
|
589
615
|
const conns = {};
|
|
590
|
-
const pattern = /^FRIDAY_CONN_(.+?)_(URL|USER|PASS)$/;
|
|
616
|
+
const pattern = /^FRIDAY_CONN_(.+?)_(URL|USER|PASS|FILEBIRD_TOKEN)$/;
|
|
591
617
|
for (const [key, value] of Object.entries(process.env)) {
|
|
592
618
|
const m = key.match(pattern);
|
|
593
619
|
if (!m) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "friday-mcp-v2",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "WordPress MCP Server for Claude Code - REST API direct communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp-server.js",
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
26
|
-
"node-fetch": "^3.3.2",
|
|
27
26
|
"marked": "^15.0.0",
|
|
27
|
+
"node-fetch": "^3.3.2",
|
|
28
|
+
"node-html-parser": "^7.1.0",
|
|
28
29
|
"ws": "^8.19.0"
|
|
29
30
|
}
|
|
30
31
|
}
|