wp-devdocs-mcp 1.1.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.
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { listDocs, getDocCategoryCounts } from '../../db/sqlite.js';
3
+
4
+ export const listDocsSchema = {
5
+ name: 'list_docs',
6
+ description: 'List available WordPress documentation pages, optionally filtered by type, category, or source. Returns titles and slugs grouped by category. Use search_docs for full-text search or get_doc to read a specific page. Tip: filter by category for complete listings — unfiltered results may be truncated.',
7
+ inputSchema: {
8
+ doc_type: z.enum(['guide', 'tutorial', 'reference', 'api', 'howto', 'faq', 'general']).optional().describe('Filter by document type'),
9
+ category: z.enum(['block-editor', 'plugins', 'rest-api', 'wp-cli', 'admin']).optional().describe('Filter by documentation category'),
10
+ source: z.string().optional().describe('Filter by source name'),
11
+ limit: z.number().min(1).max(200).optional().describe('Max results (default 50)'),
12
+ },
13
+ };
14
+
15
+ export function handleListDocs(args) {
16
+ try {
17
+ const limit = args.limit || 50;
18
+ const results = listDocs({
19
+ doc_type: args.doc_type,
20
+ category: args.category,
21
+ source: args.source,
22
+ limit,
23
+ });
24
+
25
+ if (results.length === 0) {
26
+ return {
27
+ content: [{ type: 'text', text: 'No documentation pages found. Check that doc sources are indexed.' }],
28
+ };
29
+ }
30
+
31
+ // Group by category
32
+ const grouped = {};
33
+ for (const doc of results) {
34
+ const cat = doc.category || 'general';
35
+ if (!grouped[cat]) grouped[cat] = [];
36
+ grouped[cat].push(doc);
37
+ }
38
+
39
+ const sections = [];
40
+ for (const [category, docs] of Object.entries(grouped)) {
41
+ const lines = docs.map(d => {
42
+ const parts = [`- **${d.title}** (${d.doc_type})`];
43
+ parts.push(` Slug: \`${d.slug}\` | Source: ${d.source_name}`);
44
+ if (d.description) parts.push(` ${d.description.slice(0, 120)}${d.description.length > 120 ? '...' : ''}`);
45
+ return parts.join('\n');
46
+ });
47
+ sections.push(`## ${category} (${docs.length})\n${lines.join('\n')}`);
48
+ }
49
+
50
+ let text = `${results.length} documentation page(s):\n\n${sections.join('\n\n')}`;
51
+
52
+ // If results hit the limit and no category filter, show full category counts
53
+ if (results.length >= limit && !args.category) {
54
+ const counts = getDocCategoryCounts();
55
+ const total = counts.reduce((sum, c) => sum + c.count, 0);
56
+ const summary = counts.map(c => ` - ${c.category || 'general'}: ${c.count} pages`).join('\n');
57
+ text += `\n\n---\n**Results truncated** (showing ${results.length} of ${total} total). All categories:\n${summary}\n\nFilter by category for complete listings.`;
58
+ }
59
+
60
+ text += '\n\n_Use get_doc with a slug to read the full document._';
61
+
62
+ return {
63
+ content: [{ type: 'text', text }],
64
+ };
65
+ } catch (err) {
66
+ return {
67
+ content: [{ type: 'text', text: `Error listing docs: ${err.message}` }],
68
+ isError: true,
69
+ };
70
+ }
71
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { searchBlockApis } from '../../db/sqlite.js';
3
+
4
+ export const searchBlockApisSchema = {
5
+ name: 'search_block_apis',
6
+ description: 'Search WordPress block registrations and JavaScript API usages (wp.blocks.*, wp.editor.*, wp.blockEditor.*, etc.) across all indexed sources.',
7
+ inputSchema: {
8
+ query: z.string().describe('Search query — block name, API call, namespace, or keyword'),
9
+ limit: z.number().min(1).max(100).optional().describe('Max results per category (default 20)'),
10
+ },
11
+ };
12
+
13
+ /**
14
+ * MCP tool handler — search block registrations and WP JS API usages.
15
+ * @param {object} args - { query, limit? }
16
+ * @returns {{ content: Array<{ type: string, text: string }>, isError?: boolean }}
17
+ */
18
+ export function handleSearchBlockApis(args) {
19
+ try {
20
+ const { blocks, apis } = searchBlockApis(args.query, { limit: args.limit || 20 });
21
+
22
+ if (blocks.length === 0 && apis.length === 0) {
23
+ return {
24
+ content: [{ type: 'text', text: `No block registrations or API usages found matching "${args.query}".` }],
25
+ };
26
+ }
27
+
28
+ const sections = [];
29
+
30
+ if (blocks.length > 0) {
31
+ const blockLines = blocks.map((b, i) => {
32
+ const lines = [
33
+ `### ${i + 1}. ${b.block_name || 'unknown'}`,
34
+ `- **Source:** ${b.source_name} | **File:** ${b.file_path}:${b.line_number}`,
35
+ ];
36
+ if (b.block_title) lines.push(`- **Title:** ${b.block_title}`);
37
+ if (b.block_category) lines.push(`- **Category:** ${b.block_category}`);
38
+ if (b.code_context) {
39
+ lines.push(`- **Context:**\n\`\`\`js\n${b.code_context.slice(0, 500)}\n\`\`\``);
40
+ }
41
+ return lines.join('\n');
42
+ }).join('\n\n');
43
+
44
+ sections.push(`## Block Registrations (${blocks.length})\n\n${blockLines}`);
45
+ }
46
+
47
+ if (apis.length > 0) {
48
+ const apiLines = apis.map((a, i) => {
49
+ const lines = [
50
+ `### ${i + 1}. ${a.api_call}`,
51
+ `- **Source:** ${a.source_name} | **File:** ${a.file_path}:${a.line_number}`,
52
+ `- **Namespace:** ${a.namespace} | **Method:** ${a.method}`,
53
+ ];
54
+ if (a.code_context) {
55
+ lines.push(`- **Context:**\n\`\`\`js\n${a.code_context.slice(0, 300)}\n\`\`\``);
56
+ }
57
+ return lines.join('\n');
58
+ }).join('\n\n');
59
+
60
+ sections.push(`## API Usages (${apis.length})\n\n${apiLines}`);
61
+ }
62
+
63
+ return {
64
+ content: [{ type: 'text', text: sections.join('\n\n') }],
65
+ };
66
+ } catch (err) {
67
+ return {
68
+ content: [{ type: 'text', text: `Error searching block APIs: ${err.message}` }],
69
+ isError: true,
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { searchDocs } from '../../db/sqlite.js';
3
+
4
+ export const searchDocsSchema = {
5
+ name: 'search_docs',
6
+ description: 'Search WordPress official documentation (handbooks, REST API docs, WP-CLI reference, block editor guides, etc.) using full-text search. Returns BM25-ranked results. Use get_doc to retrieve full content for a specific result.',
7
+ inputSchema: {
8
+ query: z.string().describe('Search query — topic, function name, concept, or keyword'),
9
+ doc_type: z.enum(['guide', 'tutorial', 'reference', 'api', 'howto', 'faq', 'general']).optional().describe('Filter by document type'),
10
+ category: z.enum(['block-editor', 'plugins', 'rest-api', 'wp-cli', 'admin']).optional().describe('Filter by documentation category'),
11
+ source: z.string().optional().describe('Filter by source name'),
12
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 20)'),
13
+ },
14
+ };
15
+
16
+ export function handleSearchDocs(args) {
17
+ try {
18
+ const results = searchDocs(args.query, {
19
+ doc_type: args.doc_type,
20
+ category: args.category,
21
+ source: args.source,
22
+ limit: args.limit || 20,
23
+ });
24
+
25
+ if (results.length === 0) {
26
+ return {
27
+ content: [{ type: 'text', text: `No documentation found matching "${args.query}". Try broader terms or check that doc sources are indexed.` }],
28
+ };
29
+ }
30
+
31
+ const formatted = results.map((d, i) => {
32
+ const lines = [
33
+ `### ${i + 1}. ${d.title}`,
34
+ `- **Type:** ${d.doc_type} | **Category:** ${d.category || 'general'} | **Source:** ${d.source_name}`,
35
+ `- **Slug:** ${d.slug}`,
36
+ ];
37
+ if (d.subcategory) lines.push(`- **Subcategory:** ${d.subcategory}`);
38
+ if (d.description) lines.push(`- **Description:** ${d.description.slice(0, 200)}${d.description.length > 200 ? '...' : ''}`);
39
+ lines.push(`- **ID:** ${d.id}`);
40
+ return lines.join('\n');
41
+ }).join('\n\n');
42
+
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: `Found ${results.length} doc(s) matching "${args.query}":\n\n${formatted}\n\n_Use get_doc with an ID or slug to read the full document._`,
47
+ }],
48
+ };
49
+ } catch (err) {
50
+ return {
51
+ content: [{ type: 'text', text: `Error searching docs: ${err.message}` }],
52
+ isError: true,
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import { searchHooks } from '../../db/sqlite.js';
3
+
4
+ export const searchHooksSchema = {
5
+ name: 'search_hooks',
6
+ description: 'Search WordPress hooks (actions/filters) across all indexed sources using full-text search. Returns BM25-ranked results with file locations, parameters, and descriptions.',
7
+ inputSchema: {
8
+ query: z.string().describe('Search query — hook name, keyword, or description fragment'),
9
+ type: z.enum(['action', 'filter', 'action_ref_array', 'filter_ref_array', 'js_action', 'js_filter']).optional().describe('Filter by hook type'),
10
+ source: z.string().optional().describe('Filter by source name'),
11
+ is_dynamic: z.boolean().optional().describe('Filter for dynamic hook names only'),
12
+ include_removed: z.boolean().optional().describe('Include soft-deleted hooks'),
13
+ limit: z.number().min(1).max(100).optional().describe('Max results (default 20)'),
14
+ },
15
+ };
16
+
17
+ /**
18
+ * MCP tool handler — search WordPress hooks using full-text search.
19
+ * @param {object} args - { query, type?, source?, is_dynamic?, include_removed?, limit? }
20
+ * @returns {{ content: Array<{ type: string, text: string }>, isError?: boolean }}
21
+ */
22
+ export function handleSearchHooks(args) {
23
+ try {
24
+ const results = searchHooks(args.query, {
25
+ type: args.type,
26
+ source: args.source,
27
+ isDynamic: args.is_dynamic,
28
+ includeRemoved: args.include_removed,
29
+ limit: args.limit || 20,
30
+ });
31
+
32
+ if (results.length === 0) {
33
+ return {
34
+ content: [{ type: 'text', text: `No hooks found matching "${args.query}". Try broader search terms or check source indexing with the CLI.` }],
35
+ };
36
+ }
37
+
38
+ const formatted = results.map((h, i) => {
39
+ const lines = [
40
+ `### ${i + 1}. ${h.name}`,
41
+ `- **Type:** ${h.type} | **Source:** ${h.source_name}`,
42
+ `- **File:** ${h.file_path}:${h.line_number}`,
43
+ ];
44
+ if (h.is_dynamic) lines.push('- **Dynamic:** yes');
45
+ if (h.status === 'removed') lines.push('- **Status:** REMOVED');
46
+ if (h.php_function) lines.push(`- **Function:** ${h.php_function}()`);
47
+ if (h.class_name) lines.push(`- **Class:** ${h.class_name}`);
48
+ if (h.params) lines.push(`- **Params:** ${h.params}`);
49
+ if (h.inferred_description) lines.push(`- **Description:** ${h.inferred_description}`);
50
+ if (h.docblock) lines.push(`- **Docblock:** ${h.docblock.slice(0, 200)}${h.docblock.length > 200 ? '...' : ''}`);
51
+ lines.push(`- **ID:** ${h.id}`);
52
+ return lines.join('\n');
53
+ }).join('\n\n');
54
+
55
+ return {
56
+ content: [{ type: 'text', text: `Found ${results.length} hook(s) matching "${args.query}":\n\n${formatted}` }],
57
+ };
58
+ } catch (err) {
59
+ return {
60
+ content: [{ type: 'text', text: `Error searching hooks: ${err.message}` }],
61
+ isError: true,
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { validateHook } from '../../db/sqlite.js';
3
+
4
+ export const validateHookSchema = {
5
+ name: 'validate_hook',
6
+ description: 'Check if a WordPress hook name is valid (exists in indexed sources). Returns VALID, NOT_FOUND, or REMOVED status with similar suggestions when not found. Use this to prevent hook name hallucination.',
7
+ inputSchema: {
8
+ hook_name: z.string().describe('Exact hook name to validate'),
9
+ },
10
+ };
11
+
12
+ /**
13
+ * MCP tool handler — validate if a hook name exists in indexed sources.
14
+ * @param {object} args - { hook_name }
15
+ * @returns {{ content: Array<{ type: string, text: string }>, isError?: boolean }}
16
+ */
17
+ export function handleValidateHook(args) {
18
+ try {
19
+ const result = validateHook(args.hook_name);
20
+
21
+ if (result.status === 'VALID') {
22
+ const locations = result.hooks.map(h =>
23
+ ` - ${h.source_name}: ${h.file_path}:${h.line_number} (${h.type})`
24
+ ).join('\n');
25
+
26
+ return {
27
+ content: [{
28
+ type: 'text',
29
+ text: `VALID — Hook "${args.hook_name}" exists in ${result.hooks.length} location(s):\n${locations}`,
30
+ }],
31
+ };
32
+ }
33
+
34
+ if (result.status === 'REMOVED') {
35
+ const locations = result.hooks.map(h =>
36
+ ` - ${h.source_name}: ${h.file_path}:${h.line_number} (removed ${h.removed_at || 'unknown'})`
37
+ ).join('\n');
38
+
39
+ return {
40
+ content: [{
41
+ type: 'text',
42
+ text: `REMOVED — Hook "${args.hook_name}" was found but has been removed:\n${locations}\n\nThis hook may have been deprecated or renamed.`,
43
+ }],
44
+ };
45
+ }
46
+
47
+ // NOT_FOUND
48
+ let text = `NOT FOUND — Hook "${args.hook_name}" does not exist in any indexed source.`;
49
+
50
+ if (result.similar.length > 0) {
51
+ const suggestions = result.similar.map(s =>
52
+ ` - ${s.name} (${s.type}) [${s.source_name}]`
53
+ ).join('\n');
54
+ text += `\n\nDid you mean one of these?\n${suggestions}`;
55
+ }
56
+
57
+ return {
58
+ content: [{ type: 'text', text }],
59
+ };
60
+ } catch (err) {
61
+ return {
62
+ content: [{ type: 'text', text: `Error validating hook: ${err.message}` }],
63
+ isError: true,
64
+ };
65
+ }
66
+ }