JezK
Edit File: html-main-mcp.php
<?php /** * PYS MCP settings tab — rendered inside html-wrapper-single-page.php via * the `pys_admin_pixelyoursite_mcp` action. `$viewModel` is provided by * `\PixelYourSite\MCP\AdminPage::renderPageContent()`. * * @var array $viewModel */ namespace PixelYourSite; if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! isset( $viewModel ) || ! is_array( $viewModel ) ) { return; } $tokenState = (string) ( $viewModel['token_state'] ?? '' ); $tokenActive = 'active' === $tokenState; $tokens = is_array( $viewModel['tokens'] ?? null ) ? $viewModel['tokens'] : array(); $tokenMax = (int) ( $viewModel['token_max'] ?? 25 ); $isPro = (bool) ( $viewModel['is_pro'] ?? false ); $endpointUrl = (string) ( $viewModel['endpoint_url'] ?? '' ); $recentActivity = is_array( $viewModel['recent_activity'] ?? null ) ? $viewModel['recent_activity'] : array(); $infraChecks = is_array( $viewModel['infra_checks'] ?? null ) ? $viewModel['infra_checks'] : array(); $external_access = is_array( $viewModel['external_access'] ?? null ) ? $viewModel['external_access'] : array(); $external_on = ! empty( $external_access['enabled'] ); $adapter_found = ! empty( $external_access['adapter_detected'] ); // Compact "time ago" string for header timestamps. $humanTime = static function ( $ts ) { if ( null === $ts ) { return '—'; } $delta = time() - (int) $ts; if ( $delta < 0 ) { return date_i18n( 'Y-m-d H:i:s', (int) $ts ); } return human_time_diff( (int) $ts, time() ) . ' ago'; }; $statusIcon = static function ( $ok ) { if ( $ok ) { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="5 12 10 17 19 7"></polyline></svg>'; } return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"></line><line x1="6" y1="18" x2="18" y2="6"></line></svg>'; }; $info_icon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8h.01M12 11v5"></path></svg>'; $source_badge = static function ( $entry ) { $is_external = \PixelYourSite\MCP\Provenance::SOURCE_EXTERNAL === \PixelYourSite\MCP\Provenance::entrySource( (array) $entry ); echo '<span class="pys-mcp-source pys-mcp-source--' . ( $is_external ? 'external' : 'pys' ) . '">' . esc_html( $is_external ? __( 'External', 'pys' ) : __( 'PYS', 'pys' ) ) . '</span>'; }; ?> <div class="cards-wrapper cards-wrapper-style2 gap-24 setting-wrapper pys-mcp-admin"> <!-- ============================================================= WHAT IS MCP --> <div class="card card-style6 card-static pys-mcp-info-card"> <div class="card-header card-header-style2"> <h4 class="secondary_heading_type2"><?php esc_html_e( 'What is MCP?', 'pys' ); ?></h4> </div> <div class="card-body"> <p class="text-gray mb-24"><?php esc_html_e( 'MCP (Model Context Protocol) lets AI assistants connect directly to this site and manage PixelYourSite settings through conversation. No copy-pasting, no manual navigation. Works with Claude (claude.ai and Claude Desktop), Cursor, Windsurf, VS Code with GitHub Copilot, Claude Code, and any other MCP-compatible client.', 'pys' ); ?></p> <h4 class="secondary_heading mb-8"><?php esc_html_e( 'What your AI assistant can do with access', 'pys' ); ?></h4> <ul class="mb-24"> <li><?php esc_html_e( 'Read: audit tracking health, read pixel and event configuration, check Facebook and Pinterest CAPI status', 'pys' ); ?></li> <li><?php esc_html_e( 'Write: toggle WooCommerce and EDD events, content ID settings, automatic events, create and manage custom events', 'pys' ); ?></li> <li><?php esc_html_e( 'Write operations always require your explicit confirmation before proceeding', 'pys' ); ?></li> </ul> <h4 class="secondary_heading mb-8"><?php esc_html_e( 'How access works', 'pys' ); ?></h4> <p class="text-gray mb-24"><?php esc_html_e( 'PYS MCP uses the WordPress Abilities API to expose tools to AI assistants. Each token you generate grants an AI assistant access through a secure endpoint. Tokens can be set to read-only if you want to allow auditing without allowing changes.', 'pys' ); ?></p> <p class="text-gray mb-24"><?php esc_html_e( 'This server exposes only PixelYourSite tools. The WordPress Abilities API is a shared system. Other plugins may register their own abilities, but PYS\'s MCP endpoint is scoped to PYS tools only by design.', 'pys' ); ?></p> <p class="text-gray"><?php esc_html_e( 'By default PixelYourSite tools are only available through this server; see Other MCP servers below to change that.', 'pys' ); ?></p> </div> </div> <!-- ============================================================= TOKENS --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e('Authentication tokens', 'pys');?></h4> <?php if ( !empty( $tokens ) ) : ?> <button type="button" class="btn-small btn-gray btn-small-icon secondary_heading loadable" id="pys-mcp-revoke-all-btn"><i class="icon-delete"></i><?php _e( 'Revoke all', 'pys' ); ?></button> <?php endif; ?> </div> <div class="card-body"> <p class="text-gray mb-24"> <?php _e( 'Create a separate token per client / agent (Claude Desktop, a teammate, an automation). Each is revocable on its own; writes are attributed to the token in the activity log. Only the SHA-256 hash is stored — the raw token is shown once, at creation.', 'pys' ); ?> </p> <!-- New-token reveal (shown once, right after creation) --> <div id="pys-mcp-token-reveal" style="display:none;"> <div class="alert alert-warning mb-16"> <p><strong><?php _e( 'Copy this token now.', 'pys' ); ?></strong> <?php _e( 'It will not be shown again. We only store its SHA-256 hash on the server.', 'pys' ); ?></p> </div> <div class="example-block mb-16"> <pre class="copy_text" id="pys-mcp-token-reveal-value"><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <button type="button" class="btn btn-primary btn-primary-type2 mb-24" id="pys-mcp-dismiss-reveal-btn"><?php _e( 'I saved it — refresh list', 'pys' ); ?></button> </div> <!-- Existing tokens --> <div id="pys-mcp-token-list" class="mb-24"> <?php if ( empty( $tokens ) ) : ?> <div class="pys-mcp-empty"> <span class="pys-mcp-empty__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="4.5"></circle><path d="M10.7 12.3 19 4"></path><path d="m16 7 3 3"></path><path d="m14 9 2 2"></path></svg> </span> <p class="pys-mcp-empty__title"><?php _e( 'No tokens yet', 'pys' ); ?></p> <p class="pys-mcp-empty__text"><?php _e( 'Create your first token below to connect Claude Desktop (or any MCP client) to this site. Give each client its own token so you can revoke them one at a time.', 'pys' ); ?></p> </div> <?php else : ?> <table class="pys-mcp-activity mb-24"> <thead> <tr> <th><?php _e( 'Label', 'pys' ); ?></th> <th><?php _e( 'Created', 'pys' ); ?></th> <th><?php _e( 'Last used', 'pys' ); ?></th> <th><?php _e( 'Owner', 'pys' ); ?></th> <th></th> </tr> </thead> <tbody> <?php foreach ( $tokens as $tok ) : ?> <tr> <td><strong><?php echo esc_html( (string) ( $tok['label'] ?? '' ) ); ?></strong></td> <td class="pys-mcp-activity__when"><?php echo esc_html( $humanTime( $tok['created_at'] ?? null ) ); ?></td> <td class="pys-mcp-activity__when"><?php echo esc_html( $humanTime( $tok['last_used_at'] ?? null ) ); ?></td> <td class="pys-mcp-activity__ip"><?php echo esc_html( (string) ( $tok['owner_name'] ?? '' ) ); ?></td> <td> <button type="button" class="btn-small btn-gray secondary_heading loadable pys-mcp-revoke-one" data-id="<?php echo esc_attr( (string) ( $tok['id'] ?? '' ) ); ?>" data-label="<?php echo esc_attr( (string) ( $tok['label'] ?? '' ) ); ?>"><?php _e( 'Revoke', 'pys' ); ?></button> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> <!-- Create token --> <div class="d-flex align-items-center" style="gap: 12px; flex-wrap: wrap;"> <input type="text" class="input-standard" id="pys-mcp-token-label" style="width: auto; min-width: 300px;" maxlength="60" placeholder="<?php esc_attr_e( 'Token label (e.g. Claude Desktop)', 'pys' ); ?>"/> <button type="button" class="btn btn-primary loadable" id="pys-mcp-generate-btn"><?php _e( 'Create token', 'pys' ); ?></button> <span class="text-gray"><?php echo esc_html( sprintf( /* translators: 1: current count, 2: max */ __( '%1$d of %2$d tokens', 'pys' ), count( $tokens ), $tokenMax ) ); ?></span> </div> <div class="pys-mcp-flash alert mt-24" style="display:none;"></div> </div> </div> <!-- ============================================================= SERVER URL --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2"> <h4 class="secondary_heading_type2"><?php esc_html_e( 'MCP Server URL', 'pys' ); ?></h4> </div> <div class="card-body"> <p class="text-gray mb-16"><?php esc_html_e( 'The endpoint your MCP client connects to. Paste this URL — together with a token above — into any MCP client that accepts a server URL. The setup steps below already include it.', 'pys' ); ?></p> <p class="text-gray mb-16"><?php esc_html_e( 'This site needs to be reachable on the public internet for cloud-based clients like claude.ai or Perplexity to connect — a local-only or firewalled site will not work with those. Production sites should also use HTTPS.', 'pys' ); ?></p> <div class="example-block"> <pre class="copy_text"><?php echo esc_html( $endpointUrl ); ?><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- ========================================================= OTHER MCP SERVERS --> <div class="card card-style6 card-static<?php echo $external_on ? ' pys-mcp-external-on' : ''; ?>" id="pys-mcp-access-card" data-adapter="<?php echo $adapter_found ? '1' : '0'; ?>"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php esc_html_e( 'Other MCP servers', 'pys' ); ?><span class="pys-mcp-badge-critical" id="pys-mcp-ext-badge"<?php echo $external_on ? '' : ' style="display:none;"'; ?>><?php esc_html_e( 'Critical', 'pys' ); ?></span></h4> </div> <div class="card-body"> <div class="d-flex align-items-center"> <?php \PixelYourSite\PYS()->render_switcher_input( \PixelYourSite\MCP\Capabilities::OPTION_EXTERNAL_ACCESS_ENABLED ); ?> <h4 class="switcher-label secondary_heading ml-12 mb-0"><?php esc_html_e( 'Allow other MCP servers to use PixelYourSite tools', 'pys' ); ?></h4> </div> <p class="text-gray mt-4"><?php esc_html_e( 'Off by default: PixelYourSite tools work only through the PixelYourSite MCP server and your tokens above. Turn on to also allow the servers below, for any WordPress user with PixelYourSite access.', 'pys' ); ?></p> <ul class="pys-mcp-access"> <li> <span class="pys-mcp-access__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"></path></svg></span> <span class="pys-mcp-access__text"> <span class="pys-mcp-access__label"><?php esc_html_e( 'WordPress MCP Adapter plugin', 'pys' ); ?> <?php if ( $adapter_found ) : ?> <span class="pys-mcp-chip"><?php echo esc_html( '' !== (string) ( $external_access['adapter_version'] ?? '' ) ? sprintf( /* translators: %s: plugin version */ __( 'Detected · v%s', 'pys' ), $external_access['adapter_version'] ) : __( 'Detected', 'pys' ) ); ?></span> <?php else : ?> <span class="pys-mcp-chip pys-mcp-chip--muted"><?php esc_html_e( 'Not installed', 'pys' ); ?></span> <?php endif; ?> </span> <span class="pys-mcp-access__detail"><code><?php echo esc_html( (string) ( $external_access['adapter_path'] ?? '' ) ); ?></code></span> </span> <span class="pys-mcp-access__state pys-mcp-access__state--<?php echo $external_on ? 'warn' : 'off'; ?> js-pys-mcp-ext-state"><?php echo $external_on ? esc_html__( 'Available', 'pys' ) : esc_html__( 'Hidden', 'pys' ); ?></span> </li> <li> <span class="pys-mcp-access__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h10"></path></svg></span> <span class="pys-mcp-access__text"> <span class="pys-mcp-access__label"><?php esc_html_e( 'WordPress abilities REST API', 'pys' ); ?></span> <span class="pys-mcp-access__detail"><code><?php echo esc_html( (string) ( $external_access['rest_path'] ?? '' ) ); ?></code> · <?php esc_html_e( 'WordPress 6.9+', 'pys' ); ?></span> </span> <?php if ( ! empty( $external_access['rest_available'] ) ) : ?> <span class="pys-mcp-access__state pys-mcp-access__state--<?php echo $external_on ? 'warn' : 'off'; ?> js-pys-mcp-ext-state"><?php echo $external_on ? esc_html__( 'Available', 'pys' ) : esc_html__( 'Hidden', 'pys' ); ?></span> <?php else : ?> <span class="pys-mcp-access__state pys-mcp-access__state--off"><?php esc_html_e( 'Not available', 'pys' ); ?></span> <?php endif; ?> </li> </ul> <div class="alert alert-info mt-24" id="pys-mcp-ext-alert-off"<?php echo ( $external_on || ! $adapter_found ) ? ' style="display:none;"' : ''; ?>> <p><?php esc_html_e( 'The WordPress MCP Adapter plugin is installed. It will not see PixelYourSite tools while this is off.', 'pys' ); ?></p> </div> <div class="alert alert-warning mt-24" id="pys-mcp-ext-alert-on"<?php echo $external_on ? '' : ' style="display:none;"'; ?>> <p><strong><?php esc_html_e( 'Any WordPress user with PixelYourSite access can now run PixelYourSite tools without a token.', 'pys' ); ?></strong> <?php esc_html_e( 'PixelYourSite cannot revoke or rate-limit this. Writes are logged as', 'pys' ); ?> <span class="pys-mcp-source pys-mcp-source--external"><?php esc_html_e( 'External', 'pys' ); ?></span>. <?php esc_html_e( 'Read-only mode still applies.', 'pys' ); ?></p> </div> </div> </div> <!-- ====================================================== CONNECT YOUR AI CLIENT --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <h4 class="secondary_heading_type2"><?php esc_html_e( 'Connect your AI client', 'pys' ); ?></h4> </div> </div> <div class="card-body" style="display: block;"> <div class="gap-24"> <!-- Claude (claude.ai / Desktop app / Cowork / mobile) via custom connector --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Claude (claude.ai, Desktop app, Cowork, mobile)', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <p class="text-gray mb-16"><?php esc_html_e( 'One setup covers claude.ai, the Claude Desktop app, Claude Cowork, and the Claude mobile apps — connectors run through Anthropic\'s cloud, not your device.', 'pys' ); ?></p> <p class="text-gray mb-16"><?php esc_html_e( 'Note: fixed-token authentication via request headers is currently in beta on Claude\'s side. It works reliably, but the setup screen may change.', 'pys' ); ?></p> <ol class="mb-16"> <li><?php esc_html_e( 'Settings → Connectors → "+" → Add custom connector', 'pys' ); ?></li> <li><?php esc_html_e( 'Type: Remote', 'pys' ); ?></li> <li><?php esc_html_e( 'Name: pixelyoursite (or anything you like)', 'pys' ); ?></li> <li><?php esc_html_e( 'Paste the MCP Server URL from above', 'pys' ); ?></li> <li><?php esc_html_e( 'Advanced settings → Headers helper → add a header named Authorization with value:', 'pys' ); ?></li> </ol> <div class="example-block mb-16"> <pre class="copy_text pys-mcp-token-snippet">Bearer YOUR_TOKEN_HERE<div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <ol start="6" class="mb-16"> <li><?php esc_html_e( 'Click Add', 'pys' ); ?></li> <li><?php esc_html_e( 'In any chat: "+" → Connectors → toggle it on', 'pys' ); ?></li> </ol> <p class="text-gray"><?php esc_html_e( 'Team/Enterprise accounts: an Owner adds it once under Organization settings → Connectors → Add → Custom → Web. Members then connect from Customize → Connectors.', 'pys' ); ?></p> </div> </div> <!-- Claude Desktop config file (alternative) --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Claude Desktop (config file — alternative)', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <p class="mb-16"> <?php esc_html_e( 'Use this only if the custom connector above doesn\'t fit your setup. Add this to your claude_desktop_config.json, then fully restart Claude Desktop from the system tray. If you just created a token above, it is already filled into the snippet below — otherwise replace YOUR_TOKEN_HERE with your token.', 'pys' ); ?> </p> <ul class="mb-16"> <li><strong><?php _e( 'Windows:', 'pys' ); ?></strong> <code>%APPDATA%\Claude\claude_desktop_config.json</code></li> <li><strong><?php _e( 'macOS:', 'pys' ); ?></strong> <code>~/Library/Application Support/Claude/claude_desktop_config.json</code></li> <li><strong><?php _e( 'Linux:', 'pys' ); ?></strong> <code>~/.config/Claude/claude_desktop_config.json</code></li> </ul> <?php $endpointForJson = $endpointUrl; $isHttp = 0 === strpos( $endpointForJson, 'http://' ); $sharedArgs = " \"-y\", \"mcp-remote\",\n"; $sharedArgs .= " \"" . $endpointForJson . "\",\n"; if ( $isHttp ) { $sharedArgs .= " \"--allow-http\",\n"; } $sharedArgs .= " \"--header\", \"Authorization: Bearer YOUR_TOKEN_HERE\"\n"; $snippetWin = " \"mcpServers\": {\n"; $snippetWin .= " \"pys-free\": {\n"; $snippetWin .= " \"command\": \"cmd\",\n"; $snippetWin .= " \"args\": [\n"; $snippetWin .= " \"/c\", \"npx\",\n"; $snippetWin .= $sharedArgs; $snippetWin .= " ]\n"; $snippetWin .= " }\n"; $snippetWin .= " }\n"; $snippetUnix = " \"mcpServers\": {\n"; $snippetUnix .= " \"pys-free\": {\n"; $snippetUnix .= " \"command\": \"npx\",\n"; $snippetUnix .= " \"args\": [\n"; $snippetUnix .= $sharedArgs; $snippetUnix .= " ]\n"; $snippetUnix .= " }\n"; $snippetUnix .= " }\n"; ?> <h5 class="mb-8"><?php _e( 'Windows', 'pys' ); ?></h5> <div class="example-block mb-24"> <pre class="copy_text pys-mcp-token-snippet"><?php echo esc_html( $snippetWin ); ?> <div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <h5 class="mb-8"><?php _e( 'macOS / Linux', 'pys' ); ?></h5> <div class="example-block mb-24"> <pre class="copy_text pys-mcp-token-snippet"><?php echo esc_html( $snippetUnix ); ?> <div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <?php if ( $isHttp ) : ?> <p class="mb-16"> <code>--allow-http</code> <?php esc_html_e( 'is required because the endpoint is plain HTTP (local dev environment). Production sites should be HTTPS — drop the flag once a real cert is in place.', 'pys' ); ?> </p> <?php endif; ?> <p> <?php esc_html_e( 'If npx isn\'t found, install', 'pys' ); ?> <a href="https://nodejs.org/en" target="_blank" rel="noopener">Node.js</a> <?php esc_html_e( '(it ships with npx). On macOS/Linux a GUI-launched Claude Desktop may not inherit your shell PATH — use the full path to npx (find it with which npx) if you hit a spawn npx ENOENT error.', 'pys' ); ?> </p> </div> </div> <!-- Claude Code --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Claude Code', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <h5 class="mb-8"><?php esc_html_e( 'Run this in your terminal:', 'pys' ); ?></h5> <?php $claudeCodeCmd = 'claude mcp add --transport http pys-free ' . $endpointUrl . ' --header "Authorization: Bearer YOUR_TOKEN_HERE"'; ?> <div class="example-block"> <pre class="copy_text pys-mcp-token-snippet"><?php echo esc_html( $claudeCodeCmd ); ?><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- Cursor --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Cursor', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <h5 class="mb-8"><?php esc_html_e( 'Settings → Tools and Integrations → New MCP Server → transport type HTTP → paste the URL below, then add a header named Authorization with the value below:', 'pys' ); ?></h5> <div class="example-block mb-16"> <pre class="copy_text"><?php echo esc_html( $endpointUrl ); ?><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <div class="example-block"> <pre class="copy_text pys-mcp-token-snippet">Bearer YOUR_TOKEN_HERE<div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- Windsurf --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Windsurf', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <h5 class="mb-8"><?php esc_html_e( 'Cascade panel → hammer/wrench icon → View raw config → add this server entry:', 'pys' ); ?></h5> <?php $windsurfJson = "{\n"; $windsurfJson .= " \"mcpServers\": {\n"; $windsurfJson .= " \"pixelyoursite\": {\n"; $windsurfJson .= " \"serverUrl\": \"" . $endpointUrl . "\",\n"; $windsurfJson .= " \"headers\": {\n"; $windsurfJson .= " \"Authorization\": \"Bearer YOUR_TOKEN_HERE\"\n"; $windsurfJson .= " }\n"; $windsurfJson .= " }\n"; $windsurfJson .= " }\n"; $windsurfJson .= "}"; ?> <div class="example-block"> <pre class="copy_text pys-mcp-token-snippet"><?php echo esc_html( $windsurfJson ); ?><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- VS Code + GitHub Copilot --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'VS Code + GitHub Copilot', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <h5 class="mb-8"><?php esc_html_e( 'Command Palette → MCP: Add Server → HTTP → paste the URL below, then add a header named Authorization with the value below:', 'pys' ); ?></h5> <div class="example-block mb-16"> <pre class="copy_text"><?php echo esc_html( $endpointUrl ); ?><div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> <div class="example-block"> <pre class="copy_text pys-mcp-token-snippet">Bearer YOUR_TOKEN_HERE<div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- Not yet supported --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php esc_html_e( 'Not currently supported', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <p class="text-gray mb-8"><strong>ChatGPT:</strong> <?php esc_html_e( 'Developer Mode connectors only support sign-in (OAuth) or no authentication — there\'s no field for a fixed token, so this can\'t connect today.', 'pys' ); ?></p> <p class="text-gray"><strong><?php esc_html_e( 'Gemini app (gemini.google.com):', 'pys' ); ?></strong> <?php esc_html_e( 'custom connections can be added by URL, but the setup is built around sign-in credentials rather than a fixed token, and access is limited to certain accounts and countries. Not recommended yet — Gemini CLI (a separate developer tool) does work with a header, if you need Google\'s model specifically.', 'pys' ); ?></p> </div> </div> </div> </div> </div> <!-- ========================================================= READ-ONLY --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e('Write protection', 'pys');?></h4> </div> <div class="card-body"> <div class="gap-24"> <div> <div class="d-flex align-items-center"> <?php \PixelYourSite\PYS()->render_switcher_input( \PixelYourSite\MCP\Capabilities::OPTION_READ_ONLY_ENABLED ); ?> <h4 class="switcher-label secondary_heading ml-12 mb-0"><?php _e( 'Enable read-only mode', 'pys' ); ?></h4> </div> <p class="text-gray mt-4"> <?php _e( "Enable read-only mode to allow auditing without allowing changes: every <code>set_*</code> tool returns <code>Read-only mode is enabled.</code> instead of writing, while read tools continue to work normally.<br>This toggle applies to <strong>every connected client at once</strong> — it is not per-token, so turning it on blocks writes for all your AI assistants together, not just one.", "pys" ); ?> </p> </div> </div> </div> </div> <!-- ================================================= TEST CONNECTION + INSTRUCTIONS --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e('Connection test', 'pys');?></h4> <button type="button" class="btn btn-primary btn-primary-type2 loadable" id="pys-mcp-test-btn"><?php esc_html_e( "Run checks", "pys" ); ?></button> </div> <div class="card-body"> <ul id="pys-mcp-check-list" class="pys-mcp-checks"> <?php foreach ( $infraChecks as $check ) : $checkOk = ! empty( $check['ok'] ); $is_info = ! empty( $check['info'] ); ?> <li class="pys-mcp-check pys-mcp-check--<?php echo $is_info ? 'info' : ( $checkOk ? 'ok' : 'fail' ); ?>"> <span class="pys-mcp-check__icon" aria-hidden="true"> <?php echo $is_info ? $info_icon : $statusIcon( $checkOk ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG ?> </span> <span class="pys-mcp-check__text"> <span class="pys-mcp-check__label"><?php echo esc_html( (string) $check['label'] ); ?></span> <span class="pys-mcp-check__detail"><?php echo esc_html( (string) $check['detail'] ); ?></span> </span> </li> <?php endforeach; ?> </ul> <div class="pys-mcp-flash alert mt-24" style="display:none;"></div> </div> </div> <!-- ============================================================ TERMINAL CURL --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e('Manual end-to-end check (curl)', 'pys');?></h4> </div> <div class="card-body"> <div class="mb-24"> <?php _e( "Run this from a terminal to confirm the endpoint accepts your Bearer token. Expected output: <code>200 OK</code> with a <code>Mcp-Session-Id</code> header.", "pys" ); ?> </div> <?php $curl = 'curl -i -X POST "' . $endpointUrl . '" \\' . "\n"; $curl .= ' -H "Authorization: Bearer YOUR_TOKEN_HERE" \\' . "\n"; $curl .= ' -H "Accept: application/json, text/event-stream" \\' . "\n"; $curl .= ' -H "Content-Type: application/json" \\' . "\n"; $curl .= ' -d \'{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0.1"}}}\''; ?> <div class="example-block"> <pre class="copy_text pys-mcp-token-snippet"><?php echo esc_html( $curl ); ?> <div class="copy-icon" data-toggle="pys-popover" data-tippy-trigger="click" data-tippy-placement="bottom" data-popover_id="copied-popover"></div></pre> </div> </div> </div> <!-- =========================================================== TROUBLESHOOTING --> <div class="card card-style6"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e( 'Troubleshooting', 'pys' ); ?></h4> <?php cardCollapseSettings(); ?> </div> <div class="card-body"> <table class="pys-mcp-activity"> <thead> <tr> <th><?php _e( 'What you see', 'pys' ); ?></th> <th><?php _e( 'What to do', 'pys' ); ?></th> </tr> </thead> <tbody> <tr> <td><?php _e( '"Couldn\'t reach the MCP server"', 'pys' ); ?></td> <td><?php _e( 'Confirm your site is publicly reachable and the header/token was saved correctly.', 'pys' ); ?></td> </tr> <tr> <td><?php _e( '401 / token rejected', 'pys' ); ?></td> <td><?php _e( 'Generate a new token above and re-enter it in your client.', 'pys' ); ?></td> </tr> <tr> <td><code>spawn npx ENOENT</code></td> <td><?php _e( 'Node.js isn\'t installed or isn\'t on your PATH. Install Node.js or use the full path to npx (find it with which npx).', 'pys' ); ?></td> </tr> <tr> <td><?php _e( 'No response at all', 'pys' ); ?></td> <td><?php _e( 'Confirm wp-json is reachable from outside your network — a firewall or local-only setup will block remote clients.', 'pys' ); ?></td> </tr> <tr> <td><?php _e( 'REST route check fails above', 'pys' ); ?></td> <td><?php _e( 'A security plugin may be blocking the REST API. Temporarily disable REST API restrictions and re-run the check.', 'pys' ); ?></td> </tr> <tr> <td><?php _e( 'Assistant mentions a rate limit or "loop detected" error', 'pys' ); ?></td> <td><?php _e( 'The server is protecting itself from too many rapid or repeated calls. Wait a moment and ask again — no action needed on your end.', 'pys' ); ?></td> </tr> <tr> <td><?php _e( 'Assistant uses the WordPress MCP Adapter and sees no PixelYourSite tools', 'pys' ); ?></td> <td><?php _e( 'Expected by default. Connect it to the PixelYourSite MCP server with a token, or enable Other MCP servers above.', 'pys' ); ?></td> </tr> </tbody> </table> </div> </div> <!-- =========================================================== RECENT ACTIVITY --> <div class="card card-style6 card-static"> <div class="card-header card-header-style2 d-flex justify-content-between align-items-center"> <h4 class="secondary_heading_type2"><?php _e('Recent activity', 'pys');?></h4> <div class="d-flex align-items-center" style="gap: 8px;"> <a class="btn-small btn-gray secondary_heading" href="<?php echo esc_url( (string) ( $viewModel['log_page_url'] ?? '' ) ); ?>"><?php _e( 'View full log', 'pys' ); ?></a> <button type="button" class="btn-small btn-gray btn-small-icon secondary_heading loadable" id="pys-mcp-clear-log-btn"><i class="icon-delete"></i><?php esc_html_e( "Clear log", "pys" ); ?></button> </div> </div> <div class="card-body"> <div id="pys-mcp-activity-wrap"> <?php if ( empty( $recentActivity ) ) : ?> <p><?php esc_html_e( "No write-tool calls recorded yet.", "pys" ); ?></p> <?php else : ?> <table class="pys-mcp-activity"> <thead> <tr> <th class="pys-mcp-activity__status-col"></th> <th><?php esc_html_e( "When", "pys" ); ?></th> <th><?php esc_html_e( "Tool", "pys" ); ?></th> <th><?php esc_html_e( "Source", "pys" ); ?></th> <th><?php esc_html_e( "Token", "pys" ); ?></th> <th><?php esc_html_e( "Note", "pys" ); ?></th> <th><?php esc_html_e( "IP", "pys" ); ?></th> </tr> </thead> <tbody> <?php foreach ( $recentActivity as $entry ) : $status = (string) ( $entry['result_status'] ?? '' ); $statusOk = 'success' === $status; $is_preview = \PixelYourSite\MCP\Provenance::STATUS_PREVIEW === $status; ?> <tr> <td class="pys-mcp-activity__status-cell"> <span class="pys-mcp-status pys-mcp-status--<?php echo $is_preview ? 'preview' : ( $statusOk ? 'ok' : 'fail' ); ?>" title="<?php echo esc_attr( $status ); ?>" aria-label="<?php echo esc_attr( $status ); ?>"> <?php echo $is_preview ? $info_icon : $statusIcon( $statusOk ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG ?> </span> </td> <td class="pys-mcp-activity__when"> <?php echo esc_html( isset( $entry['ts'] ) ? date_i18n( 'Y-m-d H:i:s', (int) $entry['ts'] ) : '—' ); ?> </td> <td><code><?php echo esc_html( (string) ( $entry['tool'] ?? '' ) ); ?></code></td> <td><?php $source_badge( $entry ); ?></td> <td><?php echo esc_html( (string) ( $entry['token'] ?? '' ) ); ?></td> <td class="pys-mcp-activity__note"><?php echo esc_html( (string) ( $entry['mcp_note'] ?? '' ) ); ?></td> <td class="pys-mcp-activity__ip"><?php echo esc_html( (string) ( $entry['actor_ip'] ?? '' ) ); ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> <div class="pys-mcp-flash alert mt-24" style="display:none;"></div> </div> </div> </div> <script> (function ($) { 'use strict'; // AdminPage::renderPageContent() passes these via the view-model array. let PYS_MCP = { ajaxUrl: <?php echo wp_json_encode( $viewModel['ajax_url'] ); ?>, nonce: <?php echo wp_json_encode( $viewModel['nonce'] ); ?>, actions: { generate: <?php echo wp_json_encode( $viewModel['ajax_generate'] ); ?>, revoke: <?php echo wp_json_encode( $viewModel['ajax_revoke'] ); ?>, test: <?php echo wp_json_encode( $viewModel['ajax_test_conn'] ); ?>, clearLog: <?php echo wp_json_encode( $viewModel['ajax_clear_log'] ); ?> } }; function flash($scope, type, message) { let $flash = ($scope && $scope.length) ? $scope.closest('.card').find('.pys-mcp-flash').first() : $(); if (!$flash.length) { $flash = $('.pys-mcp-flash').first(); } $flash.removeClass('alert-success alert-danger alert-warning').addClass('alert-' + type) .text(message).show(); // auto-hide success after 3s if (type === 'success') { setTimeout(function () { $flash.fadeOut(); }, 3000); } } function ajax(action, data) { return $.post(PYS_MCP.ajaxUrl, $.extend({ action: action, nonce: PYS_MCP.nonce }, data || {})); } function statusIcon(ok) { if (ok) { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="5 12 10 17 19 7"></polyline></svg>'; } return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"></line><line x1="6" y1="18" x2="18" y2="6"></line></svg>'; } function lockBtn($btn) { $btn.prop('disabled', true).addClass('is-loading'); } function unlockBtn($btn) { $btn.prop('disabled', false).removeClass('is-loading'); } function pysConfirm(opts, onConfirm) { if (typeof $.confirm === 'function') { $.confirm({ title: opts.title, content: '<p>' + opts.content + '</p>', type: 'pys', typeAnimated: true, autoClose: 'cancelAction|10000', buttons: { yesAction: { text: opts.yesText || 'Yes', btnClass: opts.yesBtnClass || 'btn-pys btn-pys-red', action: onConfirm }, cancelAction: { text: opts.noText || 'Cancel' } } }); return; } if (window.confirm(opts.title + '\n\n' + opts.content)) { onConfirm(); } } // ---------- Inject a fresh token into the connection snippets ---------- var TOKEN_PLACEHOLDER = 'YOUR_TOKEN_HERE'; function injectTokenIntoSnippets(token) { if (!token) { return; } $('.pys-mcp-token-snippet').each(function () { let $pre = $(this); if (typeof $pre.attr('data-token-template') === 'undefined') { let original = ''; $pre.contents().each(function () { if (this.nodeType === 3) { original += this.nodeValue; } }); $pre.attr('data-token-template', original); } let filled = ($pre.attr('data-token-template') || '').split(TOKEN_PLACEHOLDER).join(token); // Swap only the text node(s); keep the trailing copy-icon element intact. $pre.contents().filter(function () { return this.nodeType === 3; }).remove(); $pre.prepend(document.createTextNode(filled)); }); } // ---------- Create token ---------- function handleGenerate(e) { let $btn = $(e.currentTarget); let label = ($('#pys-mcp-token-label').val() || '').trim(); if (!label) { flash($btn, 'warning', 'Enter a label first (e.g. the client or person this token is for).'); $('#pys-mcp-token-label').focus(); return; } lockBtn($btn); ajax(PYS_MCP.actions.generate, { label: label }) .done(function (res) { if (!res || !res.success) { flash($btn, 'danger', (res && res.data && res.data.message) || 'Failed to create token.'); return; } $('#pys-mcp-token-reveal-value').contents().filter(function () { return this.nodeType === 3; // strip any prior token text node }).remove(); $('#pys-mcp-token-reveal-value').prepend(res.data.token); $('#pys-mcp-token-reveal').show(); injectTokenIntoSnippets(res.data.token); $('#pys-mcp-token-label').val(''); flash($btn, 'success', 'Token "' + (res.data.label || '') + '" created — copy it now.'); }) .fail(function () { flash($btn, 'danger', 'Network error during create.'); }) .always(function () { unlockBtn($btn); }); } function handleRevokeOne(e) { let $btn = $(e.currentTarget); let id = $btn.data('id'); let label = $btn.data('label') || 'this token'; pysConfirm({ title: 'Revoke "' + label + '"?', content: 'The MCP client using this token will lose access immediately. Other tokens are unaffected.', yesText: 'Yes, revoke' }, function () { lockBtn($btn); ajax(PYS_MCP.actions.revoke, { id: id }) .done(function (res) { if (!res || !res.success) { flash($btn, 'danger', 'Failed to revoke token.'); return; } flash($btn, 'success', 'Token revoked. Reloading…'); setTimeout(function () { window.location.reload(); }, 500); }) .fail(function () { flash($btn, 'danger', 'Network error during revoke.'); }) .always(function () { unlockBtn($btn); }); }); } function handleRevokeAll(e) { let $btn = $(e.currentTarget); pysConfirm({ title: 'Revoke ALL tokens?', content: 'Every MCP client will lose access immediately. You will need to create and re-paste new tokens. This cannot be undone.', yesText: 'Yes, revoke all' }, function () { lockBtn($btn); ajax(PYS_MCP.actions.revoke, { all: 1 }) .done(function (res) { if (!res || !res.success) { flash($btn, 'danger', 'Failed to revoke tokens.'); return; } flash($btn, 'success', 'Revoked ' + (res.data.removed || 0) + ' tokens. Reloading…'); setTimeout(function () { window.location.reload(); }, 500); }) .fail(function () { flash($btn, 'danger', 'Network error during revoke.'); }) .always(function () { unlockBtn($btn); }); }); } // ---------- Test connection ---------- function handleTestConnection(e) { let $btn = $(e.currentTarget); lockBtn($btn); ajax(PYS_MCP.actions.test) .done(function (res) { if (!res || !res.success) { flash($btn, 'danger', 'Test connection failed.'); return; } let $list = $('#pys-mcp-check-list').empty(); (res.data.checks || []).forEach(function (c) { let ok = !!c.ok, info = !!c.info; let $icon = $('<span>', { 'class': 'pys-mcp-check__icon', 'aria-hidden': 'true' }).html(info ? <?php echo wp_json_encode( $info_icon ); ?> : statusIcon(ok)); let $label = $('<span>', { 'class': 'pys-mcp-check__label' }).text(c.label), $detail = $('<span>', { 'class': 'pys-mcp-check__detail' }).text(c.detail), $text = $('<span>', { 'class': 'pys-mcp-check__text' }).append($label, $detail); $list.append( $('<li>', { 'class': 'pys-mcp-check pys-mcp-check--' + (info ? 'info' : (ok ? 'ok' : 'fail')) }) .append($icon, $text) ); }); let allOk = (res.data.checks || []).every(function (c) { return !!c.ok; }); flash($btn, allOk ? 'success' : 'warning', allOk ? 'All checks passed.' : 'Some checks failed — see list above.'); }) .fail(function () { flash($btn, 'danger', 'Network error.'); }) .always(function () { unlockBtn($btn); }); } // ---------- Clear log ---------- function handleClearLog(e) { e.preventDefault(); let $btn = $(e.currentTarget); pysConfirm({ title: 'Clear the entire MCP activity log?', content: 'All recorded write-tool calls will be deleted. This cannot be undone.', yesText: 'Yes, clear log' }, function () { lockBtn($btn); ajax(PYS_MCP.actions.clearLog) .done(function (res) { if (!res || !res.success) { flash($btn, 'danger', 'Failed to clear log.'); return; } $('#pys-mcp-activity-wrap').html('<p>' + <?php echo wp_json_encode( __( "No write-tool calls recorded yet.", "pys" ) ); ?> + '</p>'); flash($btn, 'success', 'Deleted ' + (res.data.deleted || 0) + ' entries.'); }) .fail(function () { flash($btn, 'danger', 'Network error during clear log.'); }) .always(function () { unlockBtn($btn); }); }); } // ---------- Copy to clipboard ---------- function handleCopyToken(e) { let $btn = $(e.currentTarget); let token = $('#pys-mcp-token-reveal-value').text(); if (!token) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(token).then(function () { flash($btn, 'success', 'Token copied to clipboard.'); }, function () { flash($btn, 'warning', 'Clipboard API blocked — select the token manually.'); }); } else { flash($btn, 'warning', 'Clipboard API unavailable — select the token manually.'); } } // ---------- Other MCP servers (preview until the settings are saved) ---------- function handleExternalToggle(e) { let on = $(e.currentTarget).is(':checked'), $card = $('#pys-mcp-access-card'), adapterFound = '1' === String($card.data('adapter')); $card.toggleClass('pys-mcp-external-on', on); $card.find('.js-pys-mcp-ext-state') .text(on ? 'Available' : 'Hidden') .toggleClass('pys-mcp-access__state--warn', on) .toggleClass('pys-mcp-access__state--off', !on); $('#pys-mcp-ext-badge').toggle(on); $('#pys-mcp-ext-alert-on').toggle(on); $('#pys-mcp-ext-alert-off').toggle(!on && adapterFound); } $(function () { $(document) .on('change', '#pys-mcp-access-card input[type="checkbox"]', handleExternalToggle) .on('click', '#pys-mcp-generate-btn', handleGenerate) .on('click', '.pys-mcp-revoke-one', handleRevokeOne) .on('click', '#pys-mcp-revoke-all-btn', handleRevokeAll) .on('click', '#pys-mcp-test-btn', handleTestConnection) .on('click', '#pys-mcp-clear-log-btn', handleClearLog) .on('click', '#pys-mcp-copy-token-btn', handleCopyToken) .on('click', '#pys-mcp-dismiss-reveal-btn', function () { window.location.reload(); }); }); })(jQuery); </script>