Skip to main content
Glama

WordPress Standalone MCP Server

by diazoxide
index.js14.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ErrorCode, McpError, ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import axios from 'axios'; import fs from 'fs/promises'; import os from 'os'; // Load site config from config file async function loadSiteConfig() { let configString = process.env.WP_SITES; let configPath = process.env.WP_SITES_PATH; try { if(configPath) { configPath = configPath.replace(/^~/, os.homedir) configString = await fs.readFile(configPath, 'utf8'); } if(!configString) { throw new Error("One of WP_SITES_PATH or WP_SITES environment variable is required"); } const config = JSON.parse(configString); // Validate and normalize the config const normalizedConfig = {}; for (const [alias, site] of Object.entries(config)) { if (!site.URL || !site.USER || !site.PASS) { console.error(`Invalid configuration for site ${alias}: missing required fields`); continue; } normalizedConfig[alias.toLowerCase()] = { url: site.URL.replace(/\/$/, ''), username: site.USER, auth: site.PASS, filters: site.FILTERS || { include: [], exclude: [] } }; } return normalizedConfig; } catch (error) { if (error.code === 'ENOENT') { throw new Error(`Config file not found at: ${configPath}`); } throw new Error(`Failed to load config: ${error.message}`); } } // WordPress client class class WordPressClient { constructor(site) { const config = { baseURL: `${site.url}/wp-json`, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }; if (site.auth) { const credentials = `${site.username}:${site.auth.replace(/\s+/g, '')}`; config.headers['Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`; } this.client = axios.create(config); } async discoverEndpoints() { const response = await this.client.get('/'); const routes = response.data?.routes ?? {}; return Object.entries(routes).map(([path, info]) => ({ methods: info.methods ?? [], namespace: info.namespace ?? 'wp/v2', endpoints: [path] })); } async makeRequest(endpoint, method = 'GET', params) { const path = endpoint.replace(/^\/wp-json/, '').replace(/^\/?/, '/'); const config = { method, url: path }; if (method === 'GET' && params) { config.params = params; } else if (params) { config.data = params; } const response = await this.client.request(config); return response.data; } } // Function to generate tool name from endpoint function generateToolName(site, endpoint, method) { // Extract key parts of the endpoint const parts = endpoint.split('/').filter(p => p && !p.includes('?P<') && !p.includes('(')); // Get the resource type (usually the last non-parameter part) let resource = ''; if (parts.length >= 2) { // Take last 2 parts for context (e.g., "posts", "categories") resource = parts.slice(-2).join('_'); } else if (parts.length === 1) { resource = parts[0]; } // Clean the resource name resource = resource.replace(/[^\w]/g, '_'); // Check if endpoint has ID parameter const hasId = endpoint.includes('(?P<id>') || endpoint.includes('(?P<'); const idSuffix = hasId ? '_id' : ''; // Build tool name: site_method_resource[_id] const toolName = `${site}_${method.toLowerCase()}_${resource}${idSuffix}`; // Ensure it's under 64 characters if (toolName.length > 64) { // Truncate resource part if needed const maxResourceLen = 64 - `${site}_${method.toLowerCase()}_${idSuffix}`.length; resource = resource.substring(0, maxResourceLen); return `${site}_${method.toLowerCase()}_${resource}${idSuffix}`; } return toolName; } // Function to parse endpoint parameters from path function parseEndpointParams(endpoint) { const params = []; const regex = /\((?:\?P<(\w+)>)?([^)]+)\)/g; let match; while ((match = regex.exec(endpoint)) !== null) { const paramName = match[1] || match[2].replace(/[^\w]/g, ''); params.push({ name: paramName, required: !endpoint.includes(`(?P<${paramName}>`) || !match[0].includes('?') }); } return params; } // Function to check if tool should be included based on filters function shouldIncludeTool(toolName, filters) { // Check include filters first (if specified, only these are allowed) if (filters.include && filters.include.length > 0) { for (const pattern of filters.include) { // Check exact match if (pattern === toolName) return true; // Check regex match (patterns starting with / are treated as regex) if (pattern.startsWith('/') && pattern.endsWith('/')) { try { const regex = new RegExp(pattern.slice(1, -1)); if (regex.test(toolName)) return true; } catch (e) { console.error(`Invalid regex pattern: ${pattern}`); } } } // If include filters exist and no match, exclude return false; } // Check exclude filters if (filters.exclude && filters.exclude.length > 0) { for (const pattern of filters.exclude) { // Check exact match if (pattern === toolName) return false; // Check regex match if (pattern.startsWith('/') && pattern.endsWith('/')) { try { const regex = new RegExp(pattern.slice(1, -1)); if (regex.test(toolName)) return false; } catch (e) { console.error(`Invalid regex pattern: ${pattern}`); } } } } // If no include filters and not excluded, include it return true; } // Function to create tool definition for an endpoint function createEndpointTool(site, endpoint, method, routeInfo) { const toolName = generateToolName(site, endpoint, method); const pathParams = parseEndpointParams(endpoint); // Build input schema const properties = {}; const required = []; // Add path parameters pathParams.forEach(param => { properties[param.name] = { type: "string", description: `Path parameter: ${param.name}` }; if (param.required) { required.push(param.name); } }); // Add query/body parameters based on method if (method === 'GET') { properties.params = { type: "object", description: "Query parameters for the request" }; } else if (['POST', 'PUT', 'PATCH'].includes(method)) { properties.data = { type: "object", description: "Request body data" }; } // Check if endpoint has ID parameter const hasId = endpoint.includes('(?P<id>') || endpoint.includes('(?P<'); // Create more descriptive explanation let description = `${method} request to ${endpoint}`; // Add resource-specific descriptions if (endpoint.includes('/posts')) { if (method === 'GET' && hasId) { description = `Get a specific post by ID`; } else if (method === 'GET') { description = `List posts with optional filters`; } else if (method === 'POST') { description = `Create a new post`; } else if (method === 'PUT' || method === 'PATCH') { description = `Update an existing post`; } else if (method === 'DELETE') { description = `Delete a post`; } } else if (endpoint.includes('/pages')) { if (method === 'GET' && hasId) { description = `Get a specific page by ID`; } else if (method === 'GET') { description = `List pages with optional filters`; } else if (method === 'POST') { description = `Create a new page`; } else if (method === 'PUT' || method === 'PATCH') { description = `Update an existing page`; } else if (method === 'DELETE') { description = `Delete a page`; } } else if (endpoint.includes('/users')) { if (method === 'GET' && hasId) { description = `Get a specific user by ID`; } else if (method === 'GET') { description = `List users`; } else if (method === 'POST') { description = `Create a new user`; } else if (method === 'PUT' || method === 'PATCH') { description = `Update user details`; } else if (method === 'DELETE') { description = `Delete a user`; } } else if (endpoint.includes('/media')) { if (method === 'GET' && hasId) { description = `Get specific media item details`; } else if (method === 'GET') { description = `List media items`; } else if (method === 'POST') { description = `Upload new media`; } else if (method === 'DELETE') { description = `Delete media item`; } } else if (endpoint.includes('/categories')) { if (method === 'GET' && hasId) { description = `Get a specific category`; } else if (method === 'GET') { description = `List categories`; } else if (method === 'POST') { description = `Create a new category`; } else if (method === 'PUT' || method === 'PATCH') { description = `Update category`; } else if (method === 'DELETE') { description = `Delete a category`; } } else if (endpoint.includes('/tags')) { if (method === 'GET' && hasId) { description = `Get a specific tag`; } else if (method === 'GET') { description = `List tags`; } else if (method === 'POST') { description = `Create a new tag`; } else if (method === 'PUT' || method === 'PATCH') { description = `Update tag`; } else if (method === 'DELETE') { description = `Delete a tag`; } } // Add site and endpoint info description += ` on ${site} site. Endpoint: ${endpoint}`; return { name: toolName, description, inputSchema: { type: "object", properties, required: required.length > 0 ? required : undefined } }; } // Start the server async function main() { try { // Load configuration const siteConfig = await loadSiteConfig(); const clients = new Map(); const dynamicTools = new Map(); for (const [alias, site] of Object.entries(siteConfig)) { clients.set(alias, new WordPressClient(site)); } // Discover endpoints for all sites console.error('Discovering WordPress endpoints...'); for (const [alias, client] of clients.entries()) { try { const endpoints = await client.discoverEndpoints(); // Generate tools for each endpoint const siteInfo = siteConfig[alias]; endpoints.forEach(route => { route.endpoints.forEach(endpoint => { route.methods.forEach(method => { const tool = createEndpointTool(alias, endpoint, method, route); // Check if this tool should be included if (shouldIncludeTool(tool.name, siteInfo.filters)) { dynamicTools.set(tool.name, { site: alias, endpoint, method, tool }); } }); }); }); console.error(`Discovered ${endpoints.length} endpoints for site: ${alias}`); } catch (error) { console.error(`Failed to discover endpoints for ${alias}: ${error.message}`); } } // Initialize server const server = new Server({ name: "wp-standalone-mcp", version: "1.0.0" }, { capabilities: { tools: {} } }); // Tool definitions server.setRequestHandler(ListToolsRequestSchema, async () => { // Collect all dynamic tools const tools = []; // Add dynamic endpoint tools for (const [_, toolInfo] of dynamicTools) { tools.push(toolInfo.tool); } // Add discovery tool for convenience tools.push({ name: "wp_discover_endpoints", description: "Re-discover all available REST API endpoints on a WordPress site", inputSchema: { type: "object", properties: { site: { type: "string", description: "Site alias" } }, required: ["site"] } }); return { tools }; }); // Tool handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Handle discovery tool if (name === "wp_discover_endpoints") { const client = clients.get(args.site.toLowerCase()); if (!client) throw new McpError(ErrorCode.InvalidParams, `Unknown site: ${args.site}`); // Re-discover endpoints const endpoints = await client.discoverEndpoints(); // Update dynamic tools const siteAlias = args.site.toLowerCase(); // Remove old tools for this site for (const [toolName, toolInfo] of dynamicTools) { if (toolInfo.site === siteAlias) { dynamicTools.delete(toolName); } } // Add new tools const siteInfo = siteConfig[siteAlias]; endpoints.forEach(route => { route.endpoints.forEach(endpoint => { route.methods.forEach(method => { const tool = createEndpointTool(siteAlias, endpoint, method, route); // Check if this tool should be included if (shouldIncludeTool(tool.name, siteInfo.filters)) { dynamicTools.set(tool.name, { site: siteAlias, endpoint, method, tool }); } }); }); }); return { content: [{ type: "text", text: JSON.stringify(endpoints, null, 2) }] }; } // Check if it's a dynamic endpoint tool const toolInfo = dynamicTools.get(name); if (toolInfo) { const client = clients.get(toolInfo.site); if (!client) throw new McpError(ErrorCode.InvalidParams, `Site not found: ${toolInfo.site}`); // Build the actual endpoint path by replacing parameters let actualEndpoint = toolInfo.endpoint; const pathParams = parseEndpointParams(toolInfo.endpoint); // Replace path parameters in the endpoint pathParams.forEach(param => { if (args[param.name]) { // Replace WordPress REST route patterns with actual values const patterns = [ `(?P<${param.name}>\\d+)`, `(?P<${param.name}>[\\d]+)`, `(?P<${param.name}>[^/]+)`, `(?P<${param.name}>[\\w-]+)`, `<${param.name}>`, ]; for (const pattern of patterns) { actualEndpoint = actualEndpoint.replace(new RegExp(pattern), args[param.name]); } } }); // Make the request with appropriate parameters const requestParams = args.params || args.data; const result = await client.makeRequest(actualEndpoint, toolInfo.method, requestParams); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); }); // Start server const transport = new StdioServerTransport(); await server.connect(transport); console.error(`WordPress MCP server started with ${clients.size} site(s) configured`); } catch (error) { console.error(`Server failed to start: ${error.message}`); process.exit(1); } } main();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/diazoxide/wp-standalone-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server