@adsim/wordpress-mcp-server 4.4.0 → 4.5.1
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/README.md +48 -4
- package/dxt/manifest.json +12 -5
- package/index.js +720 -150
- package/package.json +1 -1
- package/src/pluginDetector.js +158 -0
- package/tests/unit/pluginDetector.test.js +167 -0
- package/tests/unit/tools/dynamicFiltering.test.js +136 -0
- package/tests/unit/tools/outputCompression.test.js +342 -0
- package/tests/unit/tools/pluginIntelligence.test.js +864 -0
- package/tests/unit/tools/site.test.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adsim/wordpress-mcp-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server for WordPress REST API integration. Manage posts, search content, and interact with your WordPress site through any MCP-compatible client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO Plugin Detection & Rendered Head Utilities
|
|
3
|
+
*
|
|
4
|
+
* Detects active SEO plugins (RankMath, Yoast, SEOPress) via the WP REST API
|
|
5
|
+
* discovery endpoint, retrieves the rendered <head> from plugin-specific
|
|
6
|
+
* headless endpoints, and parses the resulting HTML for SEO meta tags.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// In-process cache (Map<siteUrl, string|null>)
|
|
10
|
+
const pluginCache = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect which SEO plugin is active on a WordPress site by inspecting
|
|
14
|
+
* the REST API namespaces exposed at /wp-json/.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} siteUrl Base site URL (no trailing slash)
|
|
17
|
+
* @param {Function} fetchFn node-fetch (or compatible)
|
|
18
|
+
* @returns {Promise<string|null>} 'rankmath' | 'yoast' | 'seopress' | null
|
|
19
|
+
*/
|
|
20
|
+
export async function detectSeoPlugin(siteUrl, fetchFn) {
|
|
21
|
+
if (pluginCache.has(siteUrl)) return pluginCache.get(siteUrl);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const resp = await fetchFn(`${siteUrl}/wp-json/`, {
|
|
25
|
+
headers: { 'User-Agent': 'WordPress-MCP-Server' }
|
|
26
|
+
});
|
|
27
|
+
if (!resp.ok) { pluginCache.set(siteUrl, null); return null; }
|
|
28
|
+
const data = await resp.json();
|
|
29
|
+
const ns = data.namespaces || [];
|
|
30
|
+
|
|
31
|
+
let plugin = null;
|
|
32
|
+
if (ns.includes('rankmath/v1')) plugin = 'rankmath';
|
|
33
|
+
else if (ns.includes('yoast/v1')) plugin = 'yoast';
|
|
34
|
+
else if (ns.includes('seopress/v1')) plugin = 'seopress';
|
|
35
|
+
|
|
36
|
+
pluginCache.set(siteUrl, plugin);
|
|
37
|
+
return plugin;
|
|
38
|
+
} catch {
|
|
39
|
+
pluginCache.set(siteUrl, null);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Clear the detection cache (for tests). */
|
|
45
|
+
export function _clearPluginCache() {
|
|
46
|
+
pluginCache.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve the rendered <head> HTML from a plugin's headless endpoint.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} siteUrl Base site URL
|
|
53
|
+
* @param {string} postUrl Full permalink of the post/page
|
|
54
|
+
* @param {string} plugin 'rankmath' | 'yoast'
|
|
55
|
+
* @param {Function} fetchFn node-fetch
|
|
56
|
+
* @param {string} authBase64 Base64-encoded "user:pass"
|
|
57
|
+
* @returns {Promise<{success:boolean, head?:string, json?:object, plugin?:string, error?:string}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function getRenderedHead(siteUrl, postUrl, plugin, fetchFn, authBase64) {
|
|
60
|
+
const authHeader = `Basic ${authBase64}`;
|
|
61
|
+
|
|
62
|
+
if (plugin === 'rankmath') {
|
|
63
|
+
const resp = await fetchFn(`${siteUrl}/wp-json/rankmath/v1/getHead?url=${encodeURIComponent(postUrl)}`, {
|
|
64
|
+
headers: { 'Authorization': authHeader, 'User-Agent': 'WordPress-MCP-Server' }
|
|
65
|
+
});
|
|
66
|
+
if (!resp.ok) return { success: false, error: `RankMath API: ${resp.status}` };
|
|
67
|
+
const data = await resp.json();
|
|
68
|
+
return { success: data.success !== false, head: data.head || '', plugin: 'rankmath' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (plugin === 'yoast') {
|
|
72
|
+
const resp = await fetchFn(`${siteUrl}/wp-json/yoast/v1/get_head?url=${encodeURIComponent(postUrl)}`, {
|
|
73
|
+
headers: { 'Authorization': authHeader, 'User-Agent': 'WordPress-MCP-Server' }
|
|
74
|
+
});
|
|
75
|
+
if (!resp.ok) return { success: false, error: `Yoast API: ${resp.status}` };
|
|
76
|
+
const data = await resp.json();
|
|
77
|
+
return { success: true, head: data.html || '', json: data.json || {}, plugin: 'yoast' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { success: false, error: `Plugin ${plugin} does not support rendered head` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse a rendered <head> HTML string into structured SEO metadata.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} headHtml Raw HTML from getRenderedHead().head
|
|
87
|
+
* @returns {object} Structured metadata
|
|
88
|
+
*/
|
|
89
|
+
export function parseRenderedHead(headHtml) {
|
|
90
|
+
const result = {
|
|
91
|
+
title: null,
|
|
92
|
+
meta_description: null,
|
|
93
|
+
canonical: null,
|
|
94
|
+
og_title: null,
|
|
95
|
+
og_description: null,
|
|
96
|
+
og_image: null,
|
|
97
|
+
og_type: null,
|
|
98
|
+
twitter_card: null,
|
|
99
|
+
twitter_title: null,
|
|
100
|
+
twitter_description: null,
|
|
101
|
+
twitter_image: null,
|
|
102
|
+
robots: null,
|
|
103
|
+
schema_json_ld: []
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!headHtml) return result;
|
|
107
|
+
|
|
108
|
+
// <title>...</title>
|
|
109
|
+
const titleMatch = headHtml.match(/<title>([^<]*)<\/title>/i);
|
|
110
|
+
if (titleMatch) result.title = titleMatch[1].trim();
|
|
111
|
+
|
|
112
|
+
// <meta name="description" content="..." />
|
|
113
|
+
const descMatch = headHtml.match(/<meta\s+name=["']description["']\s+content=["']([^"']*)["']/i);
|
|
114
|
+
if (descMatch) result.meta_description = descMatch[1];
|
|
115
|
+
|
|
116
|
+
// <link rel="canonical" href="..." />
|
|
117
|
+
const canonMatch = headHtml.match(/<link\s+rel=["']canonical["']\s+href=["']([^"']*)["']/i);
|
|
118
|
+
if (canonMatch) result.canonical = canonMatch[1];
|
|
119
|
+
|
|
120
|
+
// <meta name="robots" content="..." />
|
|
121
|
+
const robotsMatch = headHtml.match(/<meta\s+name=["']robots["']\s+content=["']([^"']*)["']/i);
|
|
122
|
+
if (robotsMatch) result.robots = robotsMatch[1];
|
|
123
|
+
|
|
124
|
+
// OpenGraph metas
|
|
125
|
+
const ogPatterns = {
|
|
126
|
+
og_title: /property=["']og:title["']\s+content=["']([^"']*?)["']/i,
|
|
127
|
+
og_description: /property=["']og:description["']\s+content=["']([^"']*?)["']/i,
|
|
128
|
+
og_image: /property=["']og:image["']\s+content=["']([^"']*?)["']/i,
|
|
129
|
+
og_type: /property=["']og:type["']\s+content=["']([^"']*?)["']/i,
|
|
130
|
+
};
|
|
131
|
+
for (const [key, regex] of Object.entries(ogPatterns)) {
|
|
132
|
+
const m = headHtml.match(regex);
|
|
133
|
+
if (m) result[key] = m[1];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Twitter metas
|
|
137
|
+
const twPatterns = {
|
|
138
|
+
twitter_card: /name=["']twitter:card["']\s+content=["']([^"']*?)["']/i,
|
|
139
|
+
twitter_title: /name=["']twitter:title["']\s+content=["']([^"']*?)["']/i,
|
|
140
|
+
twitter_description: /name=["']twitter:description["']\s+content=["']([^"']*?)["']/i,
|
|
141
|
+
twitter_image: /name=["']twitter:image["']\s+content=["']([^"']*?)["']/i,
|
|
142
|
+
};
|
|
143
|
+
for (const [key, regex] of Object.entries(twPatterns)) {
|
|
144
|
+
const m = headHtml.match(regex);
|
|
145
|
+
if (m) result[key] = m[1];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// JSON-LD schemas
|
|
149
|
+
const schemaRegex = /<script\s+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
150
|
+
let schemaMatch;
|
|
151
|
+
while ((schemaMatch = schemaRegex.exec(headHtml)) !== null) {
|
|
152
|
+
try {
|
|
153
|
+
result.schema_json_ld.push(JSON.parse(schemaMatch[1]));
|
|
154
|
+
} catch { /* ignore malformed JSON-LD */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
detectSeoPlugin,
|
|
4
|
+
_clearPluginCache,
|
|
5
|
+
getRenderedHead,
|
|
6
|
+
parseRenderedHead
|
|
7
|
+
} from '../../src/pluginDetector.js';
|
|
8
|
+
|
|
9
|
+
// =========================================================================
|
|
10
|
+
// detectSeoPlugin
|
|
11
|
+
// =========================================================================
|
|
12
|
+
|
|
13
|
+
describe('detectSeoPlugin', () => {
|
|
14
|
+
beforeEach(() => { _clearPluginCache(); });
|
|
15
|
+
|
|
16
|
+
function mockFetch(namespaces, ok = true) {
|
|
17
|
+
let callCount = 0;
|
|
18
|
+
return (..._args) => {
|
|
19
|
+
callCount++;
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
ok,
|
|
22
|
+
status: ok ? 200 : 500,
|
|
23
|
+
json: () => Promise.resolve({ namespaces })
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
it('detects RankMath from namespaces', async () => {
|
|
29
|
+
const fn = mockFetch(['wp/v2', 'rankmath/v1', 'oembed/1.0']);
|
|
30
|
+
expect(await detectSeoPlugin('https://site.com', fn)).toBe('rankmath');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects Yoast from namespaces', async () => {
|
|
34
|
+
const fn = mockFetch(['wp/v2', 'yoast/v1']);
|
|
35
|
+
expect(await detectSeoPlugin('https://site.com', fn)).toBe('yoast');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('detects SEOPress from namespaces', async () => {
|
|
39
|
+
const fn = mockFetch(['wp/v2', 'seopress/v1']);
|
|
40
|
+
expect(await detectSeoPlugin('https://site.com', fn)).toBe('seopress');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns null when no SEO plugin namespace found', async () => {
|
|
44
|
+
const fn = mockFetch(['wp/v2', 'oembed/1.0']);
|
|
45
|
+
expect(await detectSeoPlugin('https://site.com', fn)).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('uses cache on second call (no extra fetch)', async () => {
|
|
49
|
+
let calls = 0;
|
|
50
|
+
const fn = () => { calls++; return Promise.resolve({ ok: true, json: () => Promise.resolve({ namespaces: ['rankmath/v1'] }) }); };
|
|
51
|
+
await detectSeoPlugin('https://cached.com', fn);
|
|
52
|
+
await detectSeoPlugin('https://cached.com', fn);
|
|
53
|
+
expect(calls).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns null on fetch error', async () => {
|
|
57
|
+
const fn = () => Promise.reject(new Error('Network error'));
|
|
58
|
+
expect(await detectSeoPlugin('https://fail.com', fn)).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// =========================================================================
|
|
63
|
+
// getRenderedHead
|
|
64
|
+
// =========================================================================
|
|
65
|
+
|
|
66
|
+
describe('getRenderedHead', () => {
|
|
67
|
+
it('calls RankMath getHead endpoint', async () => {
|
|
68
|
+
let calledUrl;
|
|
69
|
+
const fn = (url) => {
|
|
70
|
+
calledUrl = url;
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
ok: true,
|
|
73
|
+
json: () => Promise.resolve({ success: true, head: '<title>Test</title>' })
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const res = await getRenderedHead('https://site.com', 'https://site.com/hello/', 'rankmath', fn, 'dTpw');
|
|
77
|
+
expect(calledUrl).toContain('rankmath/v1/getHead');
|
|
78
|
+
expect(res.success).toBe(true);
|
|
79
|
+
expect(res.head).toBe('<title>Test</title>');
|
|
80
|
+
expect(res.plugin).toBe('rankmath');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('calls Yoast get_head endpoint', async () => {
|
|
84
|
+
let calledUrl;
|
|
85
|
+
const fn = (url) => {
|
|
86
|
+
calledUrl = url;
|
|
87
|
+
return Promise.resolve({
|
|
88
|
+
ok: true,
|
|
89
|
+
json: () => Promise.resolve({ html: '<title>Yoast</title>', json: { title: 'Yoast' } })
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const res = await getRenderedHead('https://site.com', 'https://site.com/hello/', 'yoast', fn, 'dTpw');
|
|
93
|
+
expect(calledUrl).toContain('yoast/v1/get_head');
|
|
94
|
+
expect(res.success).toBe(true);
|
|
95
|
+
expect(res.head).toBe('<title>Yoast</title>');
|
|
96
|
+
expect(res.plugin).toBe('yoast');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns error for unsupported plugin', async () => {
|
|
100
|
+
const fn = () => Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
101
|
+
const res = await getRenderedHead('https://site.com', 'https://site.com/hello/', 'seopress', fn, 'dTpw');
|
|
102
|
+
expect(res.success).toBe(false);
|
|
103
|
+
expect(res.error).toContain('does not support');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// =========================================================================
|
|
108
|
+
// parseRenderedHead
|
|
109
|
+
// =========================================================================
|
|
110
|
+
|
|
111
|
+
describe('parseRenderedHead', () => {
|
|
112
|
+
const fullHead = [
|
|
113
|
+
'<title>Mon Article Test | MonSite</title>',
|
|
114
|
+
'<meta name="description" content="Description rendue par RankMath" />',
|
|
115
|
+
'<link rel="canonical" href="https://example.com/mon-article-test" />',
|
|
116
|
+
'<meta name="robots" content="index, follow" />',
|
|
117
|
+
'<meta property="og:title" content="Mon Article Test" />',
|
|
118
|
+
'<meta property="og:description" content="OG Description" />',
|
|
119
|
+
'<meta property="og:image" content="https://example.com/image.jpg" />',
|
|
120
|
+
'<meta property="og:type" content="article" />',
|
|
121
|
+
'<meta name="twitter:card" content="summary_large_image" />',
|
|
122
|
+
'<meta name="twitter:title" content="Mon Article Test" />',
|
|
123
|
+
'<meta name="twitter:description" content="Twitter Desc" />',
|
|
124
|
+
'<meta name="twitter:image" content="https://example.com/tw.jpg" />',
|
|
125
|
+
'<script type="application/ld+json">{"@type":"Article","headline":"Mon Article Test"}</script>'
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
it('extracts all fields from a complete head', () => {
|
|
129
|
+
const r = parseRenderedHead(fullHead);
|
|
130
|
+
expect(r.title).toBe('Mon Article Test | MonSite');
|
|
131
|
+
expect(r.meta_description).toBe('Description rendue par RankMath');
|
|
132
|
+
expect(r.canonical).toBe('https://example.com/mon-article-test');
|
|
133
|
+
expect(r.robots).toBe('index, follow');
|
|
134
|
+
expect(r.og_title).toBe('Mon Article Test');
|
|
135
|
+
expect(r.og_description).toBe('OG Description');
|
|
136
|
+
expect(r.og_image).toBe('https://example.com/image.jpg');
|
|
137
|
+
expect(r.og_type).toBe('article');
|
|
138
|
+
expect(r.twitter_card).toBe('summary_large_image');
|
|
139
|
+
expect(r.twitter_title).toBe('Mon Article Test');
|
|
140
|
+
expect(r.twitter_description).toBe('Twitter Desc');
|
|
141
|
+
expect(r.twitter_image).toBe('https://example.com/tw.jpg');
|
|
142
|
+
expect(r.schema_json_ld).toHaveLength(1);
|
|
143
|
+
expect(r.schema_json_ld[0]['@type']).toBe('Article');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles minimal head (title only)', () => {
|
|
147
|
+
const r = parseRenderedHead('<title>Simple</title>');
|
|
148
|
+
expect(r.title).toBe('Simple');
|
|
149
|
+
expect(r.meta_description).toBeNull();
|
|
150
|
+
expect(r.canonical).toBeNull();
|
|
151
|
+
expect(r.schema_json_ld).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns all nulls for empty string', () => {
|
|
155
|
+
const r = parseRenderedHead('');
|
|
156
|
+
expect(r.title).toBeNull();
|
|
157
|
+
expect(r.meta_description).toBeNull();
|
|
158
|
+
expect(r.canonical).toBeNull();
|
|
159
|
+
expect(r.schema_json_ld).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('ignores malformed JSON-LD', () => {
|
|
163
|
+
const html = '<script type="application/ld+json">{invalid json}</script>';
|
|
164
|
+
const r = parseRenderedHead(html);
|
|
165
|
+
expect(r.schema_json_ld).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall, getFilteredTools } from '../../../index.js';
|
|
7
|
+
import { makeRequest, mockSuccess, mockError, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
const envBackup = {};
|
|
15
|
+
|
|
16
|
+
function saveEnv(...keys) {
|
|
17
|
+
keys.forEach(k => { envBackup[k] = process.env[k]; });
|
|
18
|
+
}
|
|
19
|
+
function restoreEnv() {
|
|
20
|
+
Object.entries(envBackup).forEach(([k, v]) => {
|
|
21
|
+
if (v === undefined) delete process.env[k];
|
|
22
|
+
else process.env[k] = v;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
fetch.mockReset();
|
|
28
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
29
|
+
saveEnv('WC_CONSUMER_KEY', 'WP_REQUIRE_APPROVAL', 'WP_ENABLE_PLUGIN_INTELLIGENCE');
|
|
30
|
+
// Default: all optional features OFF
|
|
31
|
+
delete process.env.WC_CONSUMER_KEY;
|
|
32
|
+
delete process.env.WP_REQUIRE_APPROVAL;
|
|
33
|
+
delete process.env.WP_ENABLE_PLUGIN_INTELLIGENCE;
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
consoleSpy.mockRestore();
|
|
37
|
+
restoreEnv();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// =========================================================================
|
|
41
|
+
// WooCommerce filtering
|
|
42
|
+
// =========================================================================
|
|
43
|
+
|
|
44
|
+
describe('WooCommerce filtering', () => {
|
|
45
|
+
it('hides WooCommerce tools when WC_CONSUMER_KEY absent', () => {
|
|
46
|
+
const tools = getFilteredTools();
|
|
47
|
+
const wcTools = tools.filter(t => t.name.startsWith('wc_'));
|
|
48
|
+
expect(wcTools).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('shows WooCommerce tools when WC_CONSUMER_KEY set', () => {
|
|
52
|
+
process.env.WC_CONSUMER_KEY = 'ck_test';
|
|
53
|
+
const tools = getFilteredTools();
|
|
54
|
+
const wcTools = tools.filter(t => t.name.startsWith('wc_'));
|
|
55
|
+
expect(wcTools.length).toBe(13);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// =========================================================================
|
|
60
|
+
// Editorial workflow filtering
|
|
61
|
+
// =========================================================================
|
|
62
|
+
|
|
63
|
+
describe('Editorial workflow filtering', () => {
|
|
64
|
+
it('hides editorial tools when WP_REQUIRE_APPROVAL not true', () => {
|
|
65
|
+
const tools = getFilteredTools();
|
|
66
|
+
const names = tools.map(t => t.name);
|
|
67
|
+
expect(names).not.toContain('wp_submit_for_review');
|
|
68
|
+
expect(names).not.toContain('wp_approve_post');
|
|
69
|
+
expect(names).not.toContain('wp_reject_post');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows editorial tools when WP_REQUIRE_APPROVAL=true', () => {
|
|
73
|
+
process.env.WP_REQUIRE_APPROVAL = 'true';
|
|
74
|
+
const tools = getFilteredTools();
|
|
75
|
+
const names = tools.map(t => t.name);
|
|
76
|
+
expect(names).toContain('wp_submit_for_review');
|
|
77
|
+
expect(names).toContain('wp_approve_post');
|
|
78
|
+
expect(names).toContain('wp_reject_post');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Plugin Intelligence filtering
|
|
84
|
+
// =========================================================================
|
|
85
|
+
|
|
86
|
+
describe('Plugin Intelligence filtering', () => {
|
|
87
|
+
it('hides Plugin Intelligence when WP_ENABLE_PLUGIN_INTELLIGENCE not true', () => {
|
|
88
|
+
const tools = getFilteredTools();
|
|
89
|
+
const names = tools.map(t => t.name);
|
|
90
|
+
expect(names).not.toContain('wp_get_rendered_head');
|
|
91
|
+
expect(names).not.toContain('wp_audit_schema_plugins');
|
|
92
|
+
expect(names).not.toContain('wp_get_twitter_meta');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows Plugin Intelligence when WP_ENABLE_PLUGIN_INTELLIGENCE=true', () => {
|
|
96
|
+
process.env.WP_ENABLE_PLUGIN_INTELLIGENCE = 'true';
|
|
97
|
+
const tools = getFilteredTools();
|
|
98
|
+
const piNames = ['wp_get_rendered_head', 'wp_audit_rendered_seo', 'wp_get_pillar_content', 'wp_audit_schema_plugins', 'wp_get_seo_score', 'wp_get_twitter_meta'];
|
|
99
|
+
const names = tools.map(t => t.name);
|
|
100
|
+
piNames.forEach(n => expect(names).toContain(n));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// =========================================================================
|
|
105
|
+
// Combined counts
|
|
106
|
+
// =========================================================================
|
|
107
|
+
|
|
108
|
+
describe('Combined filtering counts', () => {
|
|
109
|
+
it('returns all 85 tools when all features enabled', () => {
|
|
110
|
+
process.env.WC_CONSUMER_KEY = 'ck_test';
|
|
111
|
+
process.env.WP_REQUIRE_APPROVAL = 'true';
|
|
112
|
+
process.env.WP_ENABLE_PLUGIN_INTELLIGENCE = 'true';
|
|
113
|
+
expect(getFilteredTools()).toHaveLength(85);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns 63 tools with no optional features (85 - 13wc - 3editorial - 6pi)', () => {
|
|
117
|
+
expect(getFilteredTools()).toHaveLength(63);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// handleToolCall still works for filtered-out tools
|
|
123
|
+
// =========================================================================
|
|
124
|
+
|
|
125
|
+
describe('Filtered tools remain callable', () => {
|
|
126
|
+
it('handleToolCall works for a filtered-out WooCommerce tool (wc_list_products)', async () => {
|
|
127
|
+
// WC_CONSUMER_KEY is absent, so wc_list_products is filtered from listTools
|
|
128
|
+
const names = getFilteredTools().map(t => t.name);
|
|
129
|
+
expect(names).not.toContain('wc_list_products');
|
|
130
|
+
|
|
131
|
+
// But calling it directly should NOT throw "Unknown tool"
|
|
132
|
+
// It will fail with a WooCommerce credentials error, which is fine
|
|
133
|
+
const res = await call('wc_list_products');
|
|
134
|
+
expect(res.content[0].text).not.toContain('Unknown tool');
|
|
135
|
+
});
|
|
136
|
+
});
|