@adsim/wordpress-mcp-server 5.1.0 → 5.3.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 CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  The enterprise governance layer for Claude-to-WordPress integrations — secure, auditable, and multi-site.
12
12
 
13
- **v5.1.0 Enterprise** · 175 tools · ~1101 Vitest tests · GitHub Actions CI
13
+ **v5.3.1 Enterprise** · 176 tools · ~1109 Vitest tests · GitHub Actions CI
14
14
 
15
15
  ---
16
16
 
@@ -1300,6 +1300,22 @@ npx @modelcontextprotocol/inspector node index.js
1300
1300
 
1301
1301
  ## Changelog
1302
1302
 
1303
+ ### v5.3.1 (2026-03-11) — ACF REST Integration Fix
1304
+
1305
+ - Fix: `wp_get_post` and `wp_get_page` now include `acf_fields` from WP REST `acf` field
1306
+ - Fix: `wp_get_post_meta` includes `acf_fields` alongside meta via `?_fields=meta,acf`
1307
+ - Fix: `acf_get_fields` second fallback via `wp/v2?_fields=acf` when acf/v3 returns empty
1308
+ - Hint displayed when ACF data is empty (Show in REST API diagnostic)
1309
+ - 176 tools · ~1109 Vitest tests
1310
+
1311
+ ### v5.3.0 (2026-03-11) — ACF Fallback + Post Meta
1312
+
1313
+ - `wp_get_post_meta`: generic post meta reader — ACF, Elementor, Yoast, any custom meta
1314
+ - ACF adapter: WP Core meta fallback when acf/v3 returns 404 (ACF free support)
1315
+ - ACF adapter: diagnostic warning with causes when fields are empty
1316
+ - Auto-parse JSON meta values (_elementor_data, etc.)
1317
+ - 176 tools · ~1109 Vitest tests
1318
+
1303
1319
  ### v5.1.0 (2026-03-11) — Workflow Orchestrator
1304
1320
 
1305
1321
  - `wp_run_workflow`: execute named or custom tool sequences in a single call
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adsim/wordpress-mcp-server",
3
- "version": "5.1.0",
3
+ "version": "5.3.1",
4
4
  "description": "Enterprise WordPress MCP Server — 180 tools, modular architecture, governance, audit trail, WooCommerce, Schema.org, multilingual.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -33,9 +33,37 @@ async function handleGetFields(args, apiRequest) {
33
33
  const t0 = Date.now();
34
34
  const { id, post_type = 'posts', fields, mode = 'compact' } = args;
35
35
 
36
- const data = await apiRequest(`/${post_type}/${id}`, { basePath: '/wp-json/acf/v3' });
36
+ let acfData = {};
37
+ let source = 'acf_rest_api';
38
+ let fallbackMeta = null;
39
+
40
+ try {
41
+ const data = await apiRequest(`/${post_type}/${id}`, { basePath: '/wp-json/acf/v3' });
42
+ acfData = data?.acf ?? data ?? {};
43
+ } catch (e) {
44
+ // Fallback: read from WP Core meta if acf/v3 returns 404
45
+ if (e.message.includes('404') || e.message.includes('rest_no_route')) {
46
+ try {
47
+ const post = await apiRequest(`/${post_type}/${id}?_fields=meta`, { basePath: '/wp-json/wp/v2' });
48
+ acfData = post?.meta ?? {};
49
+ source = 'wp_core_meta';
50
+ fallbackMeta = acfData;
51
+ } catch { throw e; }
52
+ } else {
53
+ throw e;
54
+ }
55
+ }
37
56
 
38
- let acfData = data?.acf ?? data ?? {};
57
+ // Second fallback: try WP REST acf field if acf/v3 returned empty
58
+ if (Object.keys(acfData).length === 0 && source === 'acf_rest_api') {
59
+ try {
60
+ const post = await apiRequest(`/${post_type}/${id}?_fields=acf`, { basePath: '/wp-json/wp/v2' });
61
+ if (post?.acf && Object.keys(post.acf).length > 0) {
62
+ acfData = post.acf;
63
+ source = 'wp_rest_acf_field';
64
+ }
65
+ } catch { /* ignore — will show diagnostic below */ }
66
+ }
39
67
 
40
68
  // Filter to requested keys
41
69
  if (fields && fields.length > 0) {
@@ -46,8 +74,32 @@ async function handleGetFields(args, apiRequest) {
46
74
  acfData = filtered;
47
75
  }
48
76
 
77
+ // Diagnostic if empty
78
+ if (Object.keys(acfData).length === 0) {
79
+ const result = {
80
+ id, post_type, fields_count: 0, acf: {},
81
+ source,
82
+ warning: 'No ACF fields returned. Possible causes:',
83
+ causes: [
84
+ '1. Field group has Show in REST API = No → Enable in ACF Admin > Field Groups > Settings',
85
+ '2. ACF free version without REST support → Upgrade to ACF Pro or add acf_rest_api filter',
86
+ '3. Post type not in field group location rules',
87
+ '4. Post ID incorrect'
88
+ ],
89
+ };
90
+ if (fallbackMeta !== null) result.fallback_meta = fallbackMeta;
91
+ auditLog({ tool: 'acf_get_fields', action: 'read_acf_fields', target: id, status: 'empty', latency_ms: Date.now() - t0 });
92
+ return json(result);
93
+ }
94
+
95
+ const responseData = { id, post_type, fields_count: Object.keys(acfData).length, acf: acfData };
96
+ if (source !== 'acf_rest_api') {
97
+ responseData.source = source;
98
+ responseData.warning = 'ACF REST API not available. Data read from WP Core meta. Enable Show in REST API in field group settings.';
99
+ }
100
+
49
101
  const guarded = applyContextGuard(
50
- { id, post_type, fields_count: Object.keys(acfData).length, acf: acfData },
102
+ responseData,
51
103
  { toolName: 'acf_get_fields', mode, maxChars: 30000 }
52
104
  );
53
105
 
@@ -1,4 +1,4 @@
1
- // src/tools/content.js — content tools (12)
1
+ // src/tools/content.js — content tools (13)
2
2
  // Definitions + handlers (v5.0.0 refactor Step B+C)
3
3
 
4
4
  import { json, strip, buildPaginationMeta, validateBlocks } from '../shared/utils.js';
@@ -19,7 +19,9 @@ export const definitions = [
19
19
  { name: 'wp_list_pages', _category: 'content', description: 'Use to browse pages with hierarchy and templates. Supports parent filter, menu_order sort. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, parent: { type: 'number' }, orderby: { type: 'string', default: 'menu_order' }, order: { type: 'string', default: 'asc' }, search: { type: 'string' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
20
20
  { name: 'wp_get_page', _category: 'content', description: 'Use to read a single page with content and template. Read-only. Warning: Elementor pages can exceed 100k chars.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
21
21
  { name: 'wp_create_page', _category: 'content', description: 'Use to create a page (defaults to draft). Supports parent, template, menu_order. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, parent: { type: 'number', default: 0 }, template: { type: 'string' }, menu_order: { type: 'number', default: 0 }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['title', 'content'] }},
22
- { name: 'wp_update_page', _category: 'content', description: 'Use to modify any page field. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, parent: { type: 'number' }, template: { type: 'string' }, menu_order: { type: 'number' }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['id'] }}
22
+ { name: 'wp_update_page', _category: 'content', description: 'Use to modify any page field. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, parent: { type: 'number' }, template: { type: 'string' }, menu_order: { type: 'number' }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['id'] }},
23
+ { name: 'wp_get_post_meta', _category: 'content', description: 'Read post meta fields. Use for ACF data if acf/v3 unavailable, or for any custom meta (_elementor_data, _yoast_wpseo_*, etc.). Returns all meta if meta_key omitted.',
24
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', default: 'posts', description: 'posts or pages' }, meta_key: { type: 'string', description: 'Specific key. Omit for all meta.' }, parse_json: { type: 'boolean', default: true, description: 'Auto-parse JSON strings (e.g. _elementor_data)' } }, required: ['post_id'] }}
23
25
  ];
24
26
 
25
27
  export const handlers = {};
@@ -55,6 +57,8 @@ handlers['wp_get_post'] = async (args) => {
55
57
  const { content_format = 'html', fields: requestedFields } = args;
56
58
  const p = await wpApiCall(`/posts/${args.id}`);
57
59
  let postData = { id: p.id, title: p.title.rendered, content: p.content.rendered, excerpt: p.excerpt.rendered, status: p.status, date: p.date, modified: p.modified, link: p.link, slug: p.slug, categories: p.categories, tags: p.tags, author: p.author, featured_media: p.featured_media, comment_status: p.comment_status, meta: p.meta || {} };
60
+ if (p.acf && Object.keys(p.acf).length > 0) { postData.acf_fields = p.acf; }
61
+ else { postData.acf_fields = {}; postData.acf_hint = 'ACF returned empty. Verify Show in REST API is enabled in each Field Group settings in WordPress Admin.'; }
58
62
  const { url: siteUrl } = getActiveAuth();
59
63
  postData = applyContentFormat(postData, content_format, siteUrl);
60
64
  if (requestedFields && requestedFields.length > 0) postData = summarizePost(postData, requestedFields);
@@ -320,7 +324,10 @@ handlers['wp_get_page'] = async (args) => {
320
324
  const { wpApiCall, auditLog, name } = rt;
321
325
  validateInput(args, { id: { type: 'number', required: true, min: 1 } });
322
326
  const pg = await wpApiCall(`/pages/${args.id}`);
323
- result = json({ id: pg.id, title: pg.title.rendered, content: pg.content.rendered, excerpt: pg.excerpt.rendered, status: pg.status, date: pg.date, modified: pg.modified, link: pg.link, slug: pg.slug, parent: pg.parent, menu_order: pg.menu_order, template: pg.template, author: pg.author, featured_media: pg.featured_media, meta: pg.meta || {} });
327
+ const pageData = { id: pg.id, title: pg.title.rendered, content: pg.content.rendered, excerpt: pg.excerpt.rendered, status: pg.status, date: pg.date, modified: pg.modified, link: pg.link, slug: pg.slug, parent: pg.parent, menu_order: pg.menu_order, template: pg.template, author: pg.author, featured_media: pg.featured_media, meta: pg.meta || {} };
328
+ if (pg.acf && Object.keys(pg.acf).length > 0) { pageData.acf_fields = pg.acf; }
329
+ else { pageData.acf_fields = {}; pageData.acf_hint = 'ACF returned empty. Verify Show in REST API is enabled in each Field Group settings in WordPress Admin.'; }
330
+ result = json(pageData);
324
331
  auditLog({ tool: name, target: args.id, target_type: 'page', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
325
332
  return result;
326
333
  };
@@ -351,3 +358,38 @@ handlers['wp_update_page'] = async (args) => {
351
358
  auditLog({ tool: name, target: id, target_type: 'page', action: 'update', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(upd) });
352
359
  return result;
353
360
  };
361
+ handlers['wp_get_post_meta'] = async (args) => {
362
+ const t0 = Date.now();
363
+ const { wpApiCall, auditLog, name } = rt;
364
+ validateInput(args, { post_id: { type: 'number', required: true, min: 1 }, post_type: { type: 'string', enum: ['posts', 'pages'] } });
365
+ const { post_id, post_type = 'posts', meta_key, parse_json = true } = args;
366
+ let meta, acfFields = {};
367
+ try {
368
+ const post = await wpApiCall(`/${post_type}/${post_id}?_fields=meta,acf`);
369
+ meta = post?.meta ?? {};
370
+ acfFields = post?.acf ?? {};
371
+ } catch (e) {
372
+ if (post_type === 'posts' && e.message.includes('404')) {
373
+ const post = await wpApiCall(`/pages/${post_id}?_fields=meta,acf`);
374
+ meta = post?.meta ?? {};
375
+ acfFields = post?.acf ?? {};
376
+ } else { throw e; }
377
+ }
378
+ // Auto-parse JSON strings
379
+ if (parse_json) {
380
+ for (const [k, v] of Object.entries(meta)) {
381
+ if (typeof v === 'string' && v.length > 1 && (v[0] === '[' || v[0] === '{')) {
382
+ try { meta[k] = JSON.parse(v); } catch { /* keep as string */ }
383
+ }
384
+ }
385
+ }
386
+ let result;
387
+ if (meta_key) {
388
+ const value = meta[meta_key] !== undefined ? meta[meta_key] : (acfFields[meta_key] !== undefined ? acfFields[meta_key] : null);
389
+ result = json({ post_id, meta_key, value });
390
+ } else {
391
+ result = json({ post_id, meta, acf_fields: acfFields, source: Object.keys(acfFields).length > 0 ? 'acf_rest' : 'wp_meta_only' });
392
+ }
393
+ auditLog({ tool: name, target: post_id, action: 'read_meta', status: 'success', latency_ms: Date.now() - t0, params: { meta_key: meta_key || 'all' } });
394
+ return result;
395
+ };
@@ -30,7 +30,7 @@ export const toolModules = [
30
30
  fse, health, performance, schema, security, editorial,
31
31
  ];
32
32
 
33
- /** All static definitions (175 tools), unfiltered */
33
+ /** All static definitions (176 tools), unfiltered */
34
34
  export const ALL_DEFINITIONS = toolModules.flatMap(m => m.definitions);
35
35
 
36
36
  /** All handlers merged into a single lookup */
@@ -137,12 +137,50 @@ describe('ACF Adapter', () => {
137
137
  expect(data._warning).toContain('truncated');
138
138
  });
139
139
 
140
- // 5. acf_get_fields — 404
141
- it('acf_get_fields propagates 404 error', async () => {
140
+ // 5. acf_get_fields — fallback to WP Core meta on 404
141
+ it('acf_get_fields falls back to WP Core meta when acf/v3 returns 404', async () => {
142
+ let callCount = 0;
143
+ const fallbackApi = async (endpoint, opts) => {
144
+ callCount++;
145
+ if (opts?.basePath === '/wp-json/acf/v3') throw new Error('WP API 404: Not Found');
146
+ // WP Core fallback
147
+ return { meta: { hero_title: 'From Core', _elementor_data: '[]' } };
148
+ };
142
149
  const tool = acfAdapter.getTools().find(t => t.name === 'acf_get_fields');
143
- await expect(
144
- tool.handler({ id: 99999 }, mockApi(null, true))
145
- ).rejects.toThrow('404');
150
+ const result = await tool.handler({ id: 42 }, fallbackApi);
151
+ const data = parseResult(result);
152
+ expect(data.source).toBe('wp_core_meta');
153
+ expect(data.warning).toContain('ACF REST API not available');
154
+ expect(data.acf.hero_title).toBe('From Core');
155
+ expect(callCount).toBe(2);
156
+ });
157
+
158
+ // 5b. acf_get_fields — fallback via ?_fields=acf when acf/v3 returns empty
159
+ it('acf_get_fields falls back to wp/v2?_fields=acf when acf/v3 returns empty', async () => {
160
+ let calls = [];
161
+ const fallbackApi = async (endpoint, opts) => {
162
+ calls.push({ endpoint, basePath: opts?.basePath });
163
+ if (opts?.basePath === '/wp-json/acf/v3') return { acf: {} };
164
+ if (endpoint.includes('_fields=acf')) return { acf: { hero: 'from_wp_rest' } };
165
+ return {};
166
+ };
167
+ const tool = acfAdapter.getTools().find(t => t.name === 'acf_get_fields');
168
+ const result = await tool.handler({ id: 42 }, fallbackApi);
169
+ const data = parseResult(result);
170
+ expect(data.acf.hero).toBe('from_wp_rest');
171
+ expect(data.source).toBe('wp_rest_acf_field');
172
+ expect(calls).toHaveLength(2);
173
+ });
174
+
175
+ // 5c. acf_get_fields — diagnostic warning when empty
176
+ it('acf_get_fields returns diagnostic when fields are empty', async () => {
177
+ const tool = acfAdapter.getTools().find(t => t.name === 'acf_get_fields');
178
+ const result = await tool.handler({ id: 42 }, mockApi({ acf: {} }));
179
+ const data = parseResult(result);
180
+ expect(data.fields_count).toBe(0);
181
+ expect(data.warning).toContain('No ACF fields returned');
182
+ expect(data.causes).toHaveLength(4);
183
+ expect(data.causes[0]).toContain('Show in REST API');
146
184
  });
147
185
 
148
186
  // 6. acf_list_field_groups — success
@@ -111,11 +111,11 @@ describe('Combined filtering counts', () => {
111
111
  process.env.WC_CONSUMER_KEY = 'ck_test';
112
112
  process.env.WP_REQUIRE_APPROVAL = 'true';
113
113
  process.env.WP_ENABLE_PLUGIN_INTELLIGENCE = 'true';
114
- expect(getFilteredTools()).toHaveLength(175);
114
+ expect(getFilteredTools()).toHaveLength(176);
115
115
  });
116
116
 
117
117
  it('returns 145 tools with no optional features (174 - 20wc - 3editorial - 6pi)', () => {
118
- expect(getFilteredTools()).toHaveLength(146);
118
+ expect(getFilteredTools()).toHaveLength(147);
119
119
  });
120
120
  });
121
121
 
@@ -188,13 +188,13 @@ describe('WP_TOOL_CATEGORIES filtering', () => {
188
188
  process.env.WP_TOOL_CATEGORIES = '';
189
189
  const tools = getFilteredTools();
190
190
  // Same as default (no category filter) — 143 base tools
191
- expect(tools).toHaveLength(146);
191
+ expect(tools).toHaveLength(147);
192
192
  });
193
193
 
194
194
  it('WP_TOOL_CATEGORIES undefined → all tools exposed', () => {
195
195
  delete process.env.WP_TOOL_CATEGORIES;
196
196
  const tools = getFilteredTools();
197
- expect(tools).toHaveLength(146);
197
+ expect(tools).toHaveLength(147);
198
198
  });
199
199
 
200
200
  it('core tools (wp_site_info, wp_set_target) always present regardless of config', () => {
@@ -216,7 +216,7 @@ describe('WP_TOOL_CATEGORIES filtering', () => {
216
216
  });
217
217
 
218
218
  it('total TOOLS_DEFINITIONS count is 174', () => {
219
- expect(TOOLS_DEFINITIONS).toHaveLength(175);
219
+ expect(TOOLS_DEFINITIONS).toHaveLength(176);
220
220
  });
221
221
 
222
222
  it('getEnabledCategories reflects WP_TOOL_CATEGORIES', () => {
@@ -0,0 +1,105 @@
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 } from '../../../index.js';
7
+ import { mockSuccess, mockError, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
8
+
9
+ function call(name, args = {}) {
10
+ return handleToolCall(makeRequest(name, args));
11
+ }
12
+
13
+ let consoleSpy;
14
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
15
+ afterEach(() => { consoleSpy.mockRestore(); });
16
+
17
+ // =========================================================================
18
+ // wp_get_post_meta
19
+ // =========================================================================
20
+
21
+ describe('wp_get_post_meta', () => {
22
+ it('SUCCESS — returns all meta fields', async () => {
23
+ mockSuccess({ meta: { _yoast_wpseo_title: 'SEO Title', custom_field: 'value' } });
24
+
25
+ const res = await call('wp_get_post_meta', { post_id: 42 });
26
+ const data = parseResult(res);
27
+
28
+ expect(data.post_id).toBe(42);
29
+ expect(data.meta._yoast_wpseo_title).toBe('SEO Title');
30
+ expect(data.meta.custom_field).toBe('value');
31
+ });
32
+
33
+ it('SUCCESS — filters by meta_key', async () => {
34
+ mockSuccess({ meta: { _yoast_wpseo_title: 'SEO Title', other: 'x' } });
35
+
36
+ const res = await call('wp_get_post_meta', { post_id: 42, meta_key: '_yoast_wpseo_title' });
37
+ const data = parseResult(res);
38
+
39
+ expect(data.post_id).toBe(42);
40
+ expect(data.meta_key).toBe('_yoast_wpseo_title');
41
+ expect(data.value).toBe('SEO Title');
42
+ });
43
+
44
+ it('SUCCESS — auto-parses JSON strings', async () => {
45
+ mockSuccess({ meta: { _elementor_data: '[{"elType":"section"}]', plain: 'text' } });
46
+
47
+ const res = await call('wp_get_post_meta', { post_id: 10, parse_json: true });
48
+ const data = parseResult(res);
49
+
50
+ expect(Array.isArray(data.meta._elementor_data)).toBe(true);
51
+ expect(data.meta._elementor_data[0].elType).toBe('section');
52
+ expect(data.meta.plain).toBe('text');
53
+ });
54
+
55
+ it('FALLBACK — retries on pages if posts returns 404', async () => {
56
+ // First call (posts) → 404
57
+ fetch.mockImplementationOnce(() => Promise.resolve({
58
+ ok: false, status: 404,
59
+ headers: { get: () => null },
60
+ text: () => Promise.resolve('Not found')
61
+ }));
62
+ // Second call (pages) → success
63
+ mockSuccess({ meta: { page_field: 'from_pages' } });
64
+
65
+ const res = await call('wp_get_post_meta', { post_id: 5, post_type: 'posts' });
66
+ const data = parseResult(res);
67
+
68
+ expect(data.post_id).toBe(5);
69
+ expect(data.meta.page_field).toBe('from_pages');
70
+ });
71
+ });
72
+
73
+ // =========================================================================
74
+ // wp_get_post — ACF fields integration
75
+ // =========================================================================
76
+
77
+ describe('wp_get_post — ACF fields', () => {
78
+ const fullPost = (acf) => ({
79
+ id: 1, title: { rendered: 'T' }, content: { rendered: '' }, excerpt: { rendered: '' },
80
+ status: 'publish', date: '2026-01-01', modified: '2026-01-01', link: 'https://test.example.com/?p=1',
81
+ slug: 'test', categories: [], tags: [], author: 1, featured_media: 0, comment_status: 'open', meta: {},
82
+ acf,
83
+ });
84
+
85
+ it('includes acf_fields when post.acf has data', async () => {
86
+ mockSuccess(fullPost({ hero_title: 'Hello', price: 29 }));
87
+
88
+ const res = await call('wp_get_post', { id: 1 });
89
+ const data = parseResult(res);
90
+
91
+ expect(data.acf_fields.hero_title).toBe('Hello');
92
+ expect(data.acf_fields.price).toBe(29);
93
+ expect(data.acf_hint).toBeUndefined();
94
+ });
95
+
96
+ it('includes acf_hint when post.acf is empty', async () => {
97
+ mockSuccess(fullPost({}));
98
+
99
+ const res = await call('wp_get_post', { id: 1 });
100
+ const data = parseResult(res);
101
+
102
+ expect(data.acf_fields).toEqual({});
103
+ expect(data.acf_hint).toContain('Show in REST API');
104
+ });
105
+ });
@@ -80,7 +80,7 @@ describe('wp_site_info', () => {
80
80
 
81
81
  // Server info
82
82
  expect(data.server.mcp_version).toBeDefined();
83
- expect(data.server.tools_total).toBe(175);
83
+ expect(data.server.tools_total).toBe(176);
84
84
  expect(typeof data.server.tools_exposed).toBe('number');
85
85
  expect(Array.isArray(data.server.filtered_out)).toBe(true);
86
86
  });