Standard WordPress posts are wrapped in your site’s theme (headers, footers, sidebars). This code looks for a custom field called html. If it finds content there, it bypasses your theme entirely and serves the raw code as a standalone webpage. This is perfect for previewing landing pages or code prototypes without site styling getting in the way.
Add this code to your fucntions.php or your code snippet to eanble this mcp.

// ============================================================
// SECTION — SERVE RAW HTML FROM THE "html" CUSTOM FIELD
// Visiting example.com/web/my-slug outputs the stored HTML
// page directly, bypassing the theme entirely.
// ============================================================
add_action( 'template_redirect', 'wpmcp_serve_raw_html' );
function wpmcp_serve_raw_html() {
if ( ! is_singular( 'web' ) ) return;
global $post;
$html = get_post_meta( $post->ID, 'html', true );
if ( empty( $html ) ) {
// Nothing stored yet — let the theme render a fallback
return;
}
status_header( 200 );
header( 'Content-Type: text/html; charset=UTF-8' );
header( 'X-Content-Type-Options: nosniff' );
echo $html; // Raw HTML page from the custom field
exit;
}
// ============================================================
// SECTION 3 — ADMIN SETTINGS PAGE
// Settings → Web Prototypes MCP
// Shows the MCP URL, API key, copy buttons, and instructions.
// ============================================================
add_action( 'admin_menu', 'wpmcp_add_settings_page' );
function wpmcp_add_settings_page() {
add_options_page(
'Web Prototypes MCP', // Page title
'Web Prototypes MCP', // Menu label
'manage_options', // Capability
'web-prototypes-mcp', // Menu slug
'wpmcp_render_settings_page' // Callback
);
}
// Auto-generate the key on first load
add_action( 'admin_init', 'wpmcp_maybe_generate_key' );
function wpmcp_maybe_generate_key() {
if ( empty( get_option( 'wpmcp_api_key', '' ) ) ) {
update_option( 'wpmcp_api_key', wpmcp_generate_key() );
}
}
function wpmcp_generate_key() {
return 'wmcp_' . bin2hex( random_bytes( 32 ) );
}
function wpmcp_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) return;
// Handle key regeneration
if (
isset( $_POST['wpmcp_action'] ) &&
$_POST['wpmcp_action'] === 'regenerate_key' &&
check_admin_referer( 'wpmcp_regenerate', 'wpmcp_nonce' )
) {
update_option( 'wpmcp_api_key', wpmcp_generate_key() );
echo '<div class="notice notice-success is-dismissible"><p><strong>API key regenerated.</strong> Update your Claude connector with the new URL.</p></div>';
}
$api_key = get_option( 'wpmcp_api_key', '' );
$mcp_url = rest_url( 'web-mcp/v1/mcp' ) . '?key=' . rawurlencode( $api_key );
?>
<div class="wrap" id="wpmcp-settings">
<h1 style="display:flex;align-items:center;gap:10px;">
<span class="dashicons dashicons-welcome-widgets-menus" style="font-size:28px;width:28px;height:28px;"></span>
Web Prototypes MCP
</h1>
<p style="color:#666;margin-top:0;">
Connect Claude directly to your Web Prototype posts — list, create, and update HTML prototypes without leaving your Claude conversation.
</p>
<div style="background:#fff;border:1px solid #ccd0d4;border-radius:4px;padding:24px 28px;max-width:860px;margin-top:20px;">
<h2 style="margin-top:0;font-size:16px;border-bottom:1px solid #eee;padding-bottom:12px;">🔗 MCP Server Connection</h2>
<table class="form-table" style="margin-top:0;">
<tr>
<th style="width:160px;padding:12px 10px 12px 0;">MCP Server URL</th>
<td>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<code id="wpmcp-url" style="background:#f6f7f7;border:1px solid #ddd;padding:8px 12px;border-radius:3px;font-size:13px;word-break:break-all;flex:1;min-width:300px;">
<?php echo esc_html( $mcp_url ); ?>
</code>
<button type="button" class="button button-secondary" onclick="wpmcpCopy('wpmcp-url', this)">
📋 Copy URL
</button>
</div>
<p class="description" style="margin-top:6px;">
The API key is embedded in the URL — paste this entire URL into Claude's <em>Remote MCP Server URL</em> field.
</p>
</td>
</tr>
<tr>
<th style="padding:12px 10px 12px 0;">Raw API Key</th>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<code id="wpmcp-key" style="background:#f6f7f7;border:1px solid #ddd;padding:8px 12px;border-radius:3px;font-size:13px;font-family:monospace;word-break:break-all;">
<?php echo esc_html( $api_key ); ?>
</code>
<button type="button" class="button button-secondary" onclick="wpmcpCopy('wpmcp-key', this)">
📋 Copy Key
</button>
</div>
<p class="description">Keep this secret. Anyone with this key can create and modify your web prototype posts.</p>
</td>
</tr>
</table>
<form method="post" style="margin-top:12px;" onsubmit="return confirm('Regenerate API key? Your existing Claude connector will stop working until you update it with the new URL.');">
<?php wp_nonce_field( 'wpmcp_regenerate', 'wpmcp_nonce' ); ?>
<input type="hidden" name="wpmcp_action" value="regenerate_key">
<input type="submit" class="button button-secondary" value="🔄 Regenerate API Key">
</form>
</div>
<div style="background:#fff;border:1px solid #ccd0d4;border-radius:4px;padding:24px 28px;max-width:860px;margin-top:20px;">
<h2 style="margin-top:0;font-size:16px;border-bottom:1px solid #eee;padding-bottom:12px;">📖 How to Connect Claude</h2>
<ol style="line-height:2;">
<li>Open <strong>Claude.ai</strong> → click your profile → <strong>Settings</strong> → <strong>Connectors</strong></li>
<li>Click <strong>"Add custom connector"</strong></li>
<li>Set <strong>Name</strong> to: <code>Web Prototypes MCP</code></li>
<li>Paste the <strong>MCP Server URL</strong> above (with the key already included) into the <em>Remote MCP Server URL</em> field</li>
<li>Leave OAuth fields <strong>empty</strong> — authentication is handled via the URL key</li>
<li>Click <strong>Add</strong> → then <strong>Connect</strong></li>
<li>Enable the connector in a conversation via the <strong>+</strong> button → Connectors</li>
</ol>
</div>
<div style="background:#fff;border:1px solid #ccd0d4;border-radius:4px;padding:24px 28px;max-width:860px;margin-top:20px;">
<h2 style="margin-top:0;font-size:16px;border-bottom:1px solid #eee;padding-bottom:12px;">🛠 Available MCP Tools</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f6f7f7;">
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">Tool</th>
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding:8px 12px;border:1px solid #ddd;"><code>list_web_posts</code></td>
<td style="padding:8px 12px;border:1px solid #ddd;">List all web prototype posts with their IDs, slugs, status, and URLs</td>
</tr>
<tr style="background:#f9f9f9;">
<td style="padding:8px 12px;border:1px solid #ddd;"><code>get_web_post</code></td>
<td style="padding:8px 12px;border:1px solid #ddd;">Retrieve the full HTML content and details of a specific post by ID</td>
</tr>
<tr>
<td style="padding:8px 12px;border:1px solid #ddd;"><code>create_web_post</code></td>
<td style="padding:8px 12px;border:1px solid #ddd;">Create a new post with a title and full HTML page content</td>
</tr>
<tr style="background:#f9f9f9;">
<td style="padding:8px 12px;border:1px solid #ddd;"><code>update_web_post</code></td>
<td style="padding:8px 12px;border:1px solid #ddd;">Update an existing post's title, HTML content, or status</td>
</tr>
</tbody>
</table>
</div>
<?php
// Quick post listing
$posts = get_posts( [ 'post_type' => 'web', 'post_status' => 'any', 'posts_per_page' => 20, 'orderby' => 'date', 'order' => 'DESC' ] );
if ( $posts ) :
?>
<div style="background:#fff;border:1px solid #ccd0d4;border-radius:4px;padding:24px 28px;max-width:860px;margin-top:20px;">
<h2 style="margin-top:0;font-size:16px;border-bottom:1px solid #eee;padding-bottom:12px;">📄 Your Web Prototypes (<?php echo count( $posts ); ?>)</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f6f7f7;">
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">ID</th>
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">Title</th>
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">Status</th>
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">URL</th>
<th style="text-align:left;padding:8px 12px;border:1px solid #ddd;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ( $posts as $post ) :
$status_color = $post->post_status === 'publish' ? '#46b450' : '#ffb900';
?>
<tr>
<td style="padding:8px 12px;border:1px solid #ddd;color:#999;"><?php echo esc_html( $post->ID ); ?></td>
<td style="padding:8px 12px;border:1px solid #ddd;font-weight:600;"><?php echo esc_html( $post->post_title ); ?></td>
<td style="padding:8px 12px;border:1px solid #ddd;">
<span style="background:<?php echo $status_color; ?>;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">
<?php echo esc_html( $post->post_status ); ?>
</span>
</td>
<td style="padding:8px 12px;border:1px solid #ddd;">
<a href="<?php echo esc_url( get_permalink( $post->ID ) ); ?>" target="_blank" style="font-size:12px;">
/web/<?php echo esc_html( $post->post_name ); ?> ↗
</a>
</td>
<td style="padding:8px 12px;border:1px solid #ddd;">
<a href="<?php echo get_edit_post_link( $post->ID ); ?>" class="button button-small">Edit</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script>
function wpmcpCopy(elementId, btn) {
const text = document.getElementById(elementId).textContent.trim();
navigator.clipboard.writeText(text).then(function() {
const original = btn.textContent;
btn.textContent = '✅ Copied!';
btn.disabled = true;
setTimeout(function() {
btn.textContent = original;
btn.disabled = false;
}, 2000);
}).catch(function() {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.textContent = '✅ Copied!';
setTimeout(function() { btn.textContent = '📋 Copy'; }, 2000);
});
}
</script>
<?php
}
// ============================================================
// SECTION — MCP REST ENDPOINT
// POST /wp-json/web-mcp/v1/mcp?key=YOUR_API_KEY
// Implements MCP protocol version 2024-11-05 (JSON-RPC 2.0)
// ============================================================
add_action( 'rest_api_init', 'wpmcp_register_rest_routes' );
function wpmcp_register_rest_routes() {
register_rest_route( 'web-mcp/v1', '/mcp', [
[
'methods' => WP_REST_Server::READABLE, // GET — for discovery
'callback' => 'wpmcp_handle_get',
'permission_callback' => '__return_true',
],
[
'methods' => WP_REST_Server::CREATABLE, // POST — JSON-RPC
'callback' => 'wpmcp_handle_post',
'permission_callback' => '__return_true',
],
] );
}
// ------------------------------------------------------------------
// Auth helper — validates the ?key= query param or Bearer header
// ------------------------------------------------------------------
function wpmcp_is_authenticated( WP_REST_Request $request ) {
$stored_key = get_option( 'wpmcp_api_key', '' );
if ( empty( $stored_key ) ) return false;
// 1) Query parameter: ?key=
$url_key = $request->get_param( 'key' );
if ( ! empty( $url_key ) && hash_equals( $stored_key, $url_key ) ) {
return true;
}
// 2) Authorization: Bearer <key> header
$auth = $request->get_header( 'authorization' );
if ( ! empty( $auth ) && preg_match( '/^Bearers+(.+)$/i', trim( $auth ), $matches ) ) {
if ( hash_equals( $stored_key, trim( $matches[1] ) ) ) {
return true;
}
}
return false;
}
// ------------------------------------------------------------------
// GET handler — returns basic server info for discovery/health check
// ------------------------------------------------------------------
function wpmcp_handle_get( WP_REST_Request $request ) {
if ( ! wpmcp_is_authenticated( $request ) ) {
return new WP_REST_Response( [ 'error' => 'Unauthorized' ], 401 );
}
return new WP_REST_Response( [
'name' => 'Web Prototypes MCP',
'version' => '1.0.0',
'protocolVersion' => '2024-11-05',
'description' => 'MCP server for managing WordPress Web Prototype posts.',
], 200 );
}
// ------------------------------------------------------------------
// POST handler — JSON-RPC 2.0 dispatcher
// ------------------------------------------------------------------
function wpmcp_handle_post( WP_REST_Request $request ) {
if ( ! wpmcp_is_authenticated( $request ) ) {
return new WP_REST_Response( wpmcp_error( null, -32000, 'Unauthorized: invalid or missing API key.' ), 401 );
}
$body = $request->get_body();
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_REST_Response( wpmcp_error( null, -32700, 'Parse error: invalid JSON.' ), 400 );
}
if ( ! isset( $data['method'] ) ) {
return new WP_REST_Response( wpmcp_error( null, -32600, 'Invalid Request: missing "method".' ), 400 );
}
$id = $data['id'] ?? null;
$method = $data['method'];
$params = $data['params'] ?? [];
// Notifications (no id, no response body needed)
$is_notification = ! array_key_exists( 'id', $data );
switch ( $method ) {
case 'initialize':
$response = wpmcp_rpc_initialize( $id, $params );
break;
case 'notifications/initialized':
// Notification — no response
return new WP_REST_Response( null, 204 );
case 'ping':
$response = [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => new stdClass() ];
break;
case 'tools/list':
$response = wpmcp_rpc_tools_list( $id );
break;
case 'tools/call':
$response = wpmcp_rpc_tools_call( $id, $params );
break;
default:
if ( $is_notification ) return new WP_REST_Response( null, 204 );
$response = wpmcp_error( $id, -32601, 'Method not found: ' . esc_html( $method ) );
break;
}
return new WP_REST_Response( $response, 200 );
}
// ============================================================
// SECTION — MCP RPC METHODS
// ============================================================
// ------------------------------------------------------------------
// initialize
// ------------------------------------------------------------------
function wpmcp_rpc_initialize( $id, $params ) {
return [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'protocolVersion' => '2024-11-05',
'capabilities' => [
'tools' => [ 'listChanged' => false ],
],
'serverInfo' => [
'name' => 'Web Prototypes MCP',
'version' => '1.0.0',
],
],
];
}
// ------------------------------------------------------------------
// tools/list — declares all available tools with full JSON Schema
// ------------------------------------------------------------------
function wpmcp_rpc_tools_list( $id ) {
$tools = [
// ── list_web_posts ────────────────────────────────────────
[
'name' => 'list_web_posts',
'description' => 'List Web Prototype posts. Returns ID, title, slug, publish status, URL, and creation date for each post. Use this to discover existing prototypes before creating or updating.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'status' => [
'type' => 'string',
'description' => 'Filter by post status. Use "any" to see all posts regardless of status.',
'enum' => [ 'publish', 'draft', 'any' ],
'default' => 'any',
],
'per_page' => [
'type' => 'integer',
'description' => 'Number of posts to return (1–100). Default: 20.',
'minimum' => 1,
'maximum' => 100,
'default' => 20,
],
],
'required' => [],
],
],
// ── get_web_post ──────────────────────────────────────────
[
'name' => 'get_web_post',
'description' => 'Retrieve the complete details and full HTML content of a single Web Prototype post by its WordPress post ID. Use this before updating a post so you can review its current content.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_id' => [
'type' => 'integer',
'description' => 'The WordPress post ID (get it from list_web_posts).',
],
],
'required' => [ 'post_id' ],
],
],
// ── create_web_post ───────────────────────────────────────
[
'name' => 'create_web_post',
'description' => 'Create a new Web Prototype post. The full HTML page (including <!DOCTYPE>, <head>, <body>, CSS, JS) is saved to the "html" custom field and served at example.com/web/{slug}. Returns the new post ID and live URL.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'title' => [
'type' => 'string',
'description' => 'The title of the web prototype post.',
],
'html' => [
'type' => 'string',
'description' => 'The complete HTML content to store. Can be a full HTML page with <!DOCTYPE html>, <head>, styles, scripts, and <body>.',
],
'slug' => [
'type' => 'string',
'description' => 'Optional. Custom URL slug (e.g. "my-prototype"). If omitted, auto-generated from the title. The post will be live at example.com/web/{slug}.',
],
'status' => [
'type' => 'string',
'description' => 'Post status. Use "publish" (default) to make it immediately accessible, or "draft" to save without publishing.',
'enum' => [ 'publish', 'draft' ],
'default' => 'publish',
],
],
'required' => [ 'title', 'html' ],
],
],
// ── update_web_post ───────────────────────────────────────
[
'name' => 'update_web_post',
'description' => 'Update an existing Web Prototype post. You can change its title, replace the entire HTML content, or change its publish status. Provide only the fields you want to change; omitted fields are left unchanged.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'post_id' => [
'type' => 'integer',
'description' => 'The WordPress post ID of the prototype to update (get it from list_web_posts).',
],
'title' => [
'type' => 'string',
'description' => 'New title for the post (optional).',
],
'html' => [
'type' => 'string',
'description' => 'New complete HTML content to replace the existing content (optional). Replaces the entire html custom field.',
],
'status' => [
'type' => 'string',
'description' => 'New post status (optional). Use "publish" or "draft".',
'enum' => [ 'publish', 'draft' ],
],
],
'required' => [ 'post_id' ],
],
],
];
return [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [ 'tools' => $tools ],
];
}
// ------------------------------------------------------------------
// tools/call — dispatches to individual tool handlers
// ------------------------------------------------------------------
function wpmcp_rpc_tools_call( $id, $params ) {
$name = $params['name'] ?? '';
$arguments = $params['arguments'] ?? [];
switch ( $name ) {
case 'list_web_posts': return wpmcp_tool_list_posts( $id, $arguments );
case 'get_web_post': return wpmcp_tool_get_post( $id, $arguments );
case 'create_web_post': return wpmcp_tool_create_post( $id, $arguments );
case 'update_web_post': return wpmcp_tool_update_post( $id, $arguments );
default:
return wpmcp_error( $id, -32602, 'Unknown tool: ' . esc_html( $name ) );
}
}
// ============================================================
// SECTION — TOOL IMPLEMENTATIONS
// ============================================================
// ------------------------------------------------------------------
// Tool: list_web_posts
// ------------------------------------------------------------------
function wpmcp_tool_list_posts( $id, $args ) {
$status = in_array( $args['status'] ?? 'any', [ 'publish', 'draft', 'any' ], true )
? $args['status']
: 'any';
$per_page = min( max( (int) ( $args['per_page'] ?? 20 ), 1 ), 100 );
$posts = get_posts( [
'post_type' => 'web',
'post_status' => $status,
'posts_per_page' => $per_page,
'orderby' => 'date',
'order' => 'DESC',
] );
$items = [];
foreach ( $posts as $p ) {
$items[] = [
'post_id' => $p->ID,
'title' => $p->post_title,
'slug' => $p->post_name,
'status' => $p->post_status,
'url' => get_permalink( $p->ID ),
'created_at' => $p->post_date,
'modified_at'=> $p->post_modified,
];
}
$text = empty( $items )
? 'No web prototype posts found.'
: 'Found ' . count( $items ) . " web prototype(s):nn" . json_encode( $items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
return wpmcp_tool_result( $id, $text );
}
// ------------------------------------------------------------------
// Tool: get_web_post
// ------------------------------------------------------------------
function wpmcp_tool_get_post( $id, $args ) {
$post_id = (int) ( $args['post_id'] ?? 0 );
if ( ! $post_id ) {
return wpmcp_error( $id, -32602, 'Missing required argument: post_id.' );
}
$post = get_post( $post_id );
if ( ! $post || $post->post_type !== 'web' ) {
return wpmcp_error( $id, -32000, "Post not found or not a 'web' post type. ID: {$post_id}" );
}
$html = get_post_meta( $post_id, 'html', true );
$info = [
'post_id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'status' => $post->post_status,
'url' => get_permalink( $post_id ),
'created_at' => $post->post_date,
'modified_at' => $post->post_modified,
'html_length' => strlen( $html ?? '' ),
];
$text = "Post details:n" . json_encode( $info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
$text .= "nn--- HTML CONTENT ---n";
$text .= ! empty( $html ) ? $html : '(no HTML content saved yet)';
return wpmcp_tool_result( $id, $text );
}
// ------------------------------------------------------------------
// Tool: create_web_post
// ------------------------------------------------------------------
function wpmcp_tool_create_post( $id, $args ) {
$title = trim( $args['title'] ?? '' );
$html = $args['html'] ?? '';
if ( empty( $title ) ) {
return wpmcp_error( $id, -32602, 'Missing required argument: title.' );
}
if ( empty( $html ) ) {
return wpmcp_error( $id, -32602, 'Missing required argument: html.' );
}
$status = in_array( $args['status'] ?? 'publish', [ 'publish', 'draft' ], true )
? $args['status']
: 'publish';
$post_data = [
'post_type' => 'web',
'post_title' => wp_strip_all_tags( $title ),
'post_status' => $status,
'post_author' => get_current_user_id() ?: 1,
'post_content'=> '',
];
// Custom slug
if ( ! empty( $args['slug'] ) ) {
$post_data['post_name'] = sanitize_title( $args['slug'] );
}
$post_id = wp_insert_post( $post_data, true );
if ( is_wp_error( $post_id ) ) {
return wpmcp_error( $id, -32000, 'Failed to create post: ' . $post_id->get_error_message() );
}
// Save full HTML to the "html" custom field (unsanitized — full pages allowed)
update_post_meta( $post_id, 'html', $html );
$post = get_post( $post_id );
$url = get_permalink( $post_id );
$result = [
'success' => true,
'post_id' => $post_id,
'title' => $post->post_title,
'slug' => $post->post_name,
'status' => $post->post_status,
'url' => $url,
];
$text = "Web prototype created successfully!nn" . json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
return wpmcp_tool_result( $id, $text );
}
// ------------------------------------------------------------------
// Tool: update_web_post
// ------------------------------------------------------------------
function wpmcp_tool_update_post( $id, $args ) {
$post_id = (int) ( $args['post_id'] ?? 0 );
if ( ! $post_id ) {
return wpmcp_error( $id, -32602, 'Missing required argument: post_id.' );
}
$post = get_post( $post_id );
if ( ! $post || $post->post_type !== 'web' ) {
return wpmcp_error( $id, -32000, "Post not found or not a 'web' post type. ID: {$post_id}" );
}
$update_data = [ 'ID' => $post_id ];
$updated = [];
if ( isset( $args['title'] ) && $args['title'] !== '' ) {
$update_data['post_title'] = wp_strip_all_tags( $args['title'] );
$updated[] = 'title';
}
if ( isset( $args['status'] ) && in_array( $args['status'], [ 'publish', 'draft' ], true ) ) {
$update_data['post_status'] = $args['status'];
$updated[] = 'status';
}
if ( count( $update_data ) > 1 ) {
$result = wp_update_post( $update_data, true );
if ( is_wp_error( $result ) ) {
return wpmcp_error( $id, -32000, 'Failed to update post: ' . $result->get_error_message() );
}
}
if ( isset( $args['html'] ) ) {
update_post_meta( $post_id, 'html', $args['html'] );
$updated[] = 'html';
}
if ( empty( $updated ) ) {
return wpmcp_error( $id, -32602, 'No fields provided to update. Supply at least one of: title, html, status.' );
}
$post = get_post( $post_id );
$result = [
'success' => true,
'post_id' => $post_id,
'title' => $post->post_title,
'slug' => $post->post_name,
'status' => $post->post_status,
'url' => get_permalink( $post_id ),
'fields_updated' => $updated,
];
$text = "Web prototype updated successfully!nn" . json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
return wpmcp_tool_result( $id, $text );
}
// ============================================================
// SECTION — SHARED HELPERS
// ============================================================
/**
* Build a JSON-RPC success response with a text content block.
*/
function wpmcp_tool_result( $id, string $text ) {
return [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
[ 'type' => 'text', 'text' => $text ],
],
'isError' => false,
],
];
}
/**
* Build a JSON-RPC error response.
*/
function wpmcp_error( $id, int $code, string $message ) {
return [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => $code,
'message' => $message,
],
];
}
// ============================================================
// SECTION — FORCE JSON RESPONSE HEADERS FOR MCP ENDPOINT
// WordPress adds charset info that some MCP clients dislike;
// ensure a clean application/json Content-Type.
// ============================================================
add_filter( 'rest_pre_serve_request', 'wpmcp_fix_json_headers', 10, 4 );
function wpmcp_fix_json_headers( $served, $result, $request, $server ) {
if ( strpos( $request->get_route(), '/web-mcp/v1/mcp' ) === false ) {
return $served;
}
header( 'Content-Type: application/json; charset=UTF-8', true );
header( 'Access-Control-Allow-Origin: *' );
header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
header( 'Access-Control-Allow-Headers: Authorization, Content-Type' );
return $served;
}
// Handle OPTIONS preflight for CORS
add_action( 'rest_api_init', function() {
if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' &&
isset( $_SERVER['REQUEST_URI'] ) &&
strpos( $_SERVER['REQUEST_URI'], '/web-mcp/v1/mcp' ) !== false
) {
header( 'Access-Control-Allow-Origin: *' );
header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
header( 'Access-Control-Allow-Headers: Authorization, Content-Type' );
header( 'HTTP/1.1 204 No Content' );
exit;
}
}, 0 );