01
Arquitectura y alcance
Esta referencia describe la implementación 0.6.0. El frontend publica herramientas en el navegador a través de FastWebMCP; las operaciones PHP se ejecutan mediante REST de WordPress. Exponer una ruta REST por sí solo no registra una herramienta WebMCP.
- Theme o bloqueMarkup, controles y estado visible.
- Runtime compartidowindow.FastWebMCP · Zod · registro nativo · ciclo de vida.
- WordPress RESTSesión · esquema · saneamiento · permisos · callback PHP.
runtime.ts contiene la API; frontend.ts, los adaptadores de búsqueda y filtros; frontend-api.php, el registro de operaciones y las rutas. Los bundles administrativos se cargan en sus pantallas correspondientes. El frontend no necesita el panel técnico.
Requisitos del plugin: WordPress 6.5+, PHP 8.0+. Para ejecutar herramientas desde un agente se necesita un navegador con WebMCP nativo y un agente capaz de invocarlas. El runtime utiliza MutationObserver, AbortController y AbortSignal.any; no incorpora un polyfill de WebMCP ni un cliente de IA.
Implementación: runtime.ts, frontend-api.php. La API nativa se describe en la especificación WebMCP.
02
Integración mínima en un theme
Con el plugin activo, carga tu extensión después de fwmcp-runtime. Este ejemplo añade un shortcode de contador. La herramienta y el botón comparten la misma función, y cada instancia mantiene su estado.
<?php
add_action('wp_enqueue_scripts', function () {
if (!function_exists('fwmcp_enqueue_frontend')) {
return;
}
fwmcp_enqueue_frontend();
wp_enqueue_script(
'mytheme-webmcp',
get_stylesheet_directory_uri() . '/webmcp.js',
array('fwmcp-runtime'),
'1.0.0',
true
);
});
add_shortcode('webmcp_counter', function () {
return '<section data-fwmcp-component="counter">
<button type="button">Sumar</button>
<output aria-live="polite">0</output>
<p data-status role="status"></p>
</section>';
});function setupCounter(api) {
api.registerComponent(
'counter',
'[data-fwmcp-component="counter"]',
({ id, root, signal, registerTool }) => {
const output = root.querySelector('output');
const status = root.querySelector('[data-status]');
let count = 0;
function increment() {
output.textContent = String(++count);
status.textContent = 'Contador actualizado.';
return { id, count };
}
root.querySelector('button').addEventListener(
'click', increment, { signal }
);
void registerTool({
name: 'increment',
description: 'Increment this visible counter by one.',
inputSchema: api.z.object({}).strict(),
execute: increment
}).catch(error => {
status.textContent = error.message;
});
return () => { /* limpiar recursos adicionales */ };
}
);
}
if (window.FastWebMCP) {
setupCounter(window.FastWebMCP);
} else {
document.addEventListener('fastwebmcp:ready', event => {
setupCounter(event.detail);
}, { once: true });
}Coloca [webmcp_counter] en un bloque Shortcode. Prueba dos instancias: sus herramientas reciben nombres como counter_5_increment y counter_6_increment. Los números son ilustrativos; descúbrelos en cada página.
El callback de montaje de registerComponent debe ser síncrono. Puede iniciar registros asíncronos y devolver una función de limpieza. Sin WebMCP, el runtime aún monta el componente y su botón manual.
03
Referencia JavaScript
| Método / propiedad | Entrada y resultado | Uso |
|---|---|---|
registerTool(spec, options?) | Promise<boolean> | true confirma registro nativo; false indica registro desactivado/no disponible o una señal ya cancelada. Los errores rechazan la Promise. |
registerComponent(type, selector, mount) | Devuelve dispose() | Asigna prefijo y señal por instancia. dispose retira el adaptador y sus instancias. |
invokeServer(name, input, options?) | Promise<JSON> | Llama a una operación PHP con validación REST y sesión. No registra herramientas. |
registerServerTool(name, options?) | Promise<boolean> | Descubre el descriptor y registra una herramienta global server_NAME. |
schemaFromJSON(schema) | Devuelve un esquema Zod | Convierte el subconjunto documentado; rechaza restricciones desconocidas. |
components() | Array<{id, type}> | Lista las instancias que conoce el runtime. No es un inventario de todas las herramientas del navegador. |
reconcile() | Sin resultado de negocio | Revisa raíces actuales y adaptadores; útil cuando cambian atributos fuera de la observación automática. |
supported() | boolean | Comprueba la configuración y la presencia de document.modelContext. No certifica la conexión del agente. |
z / version | Zod compartido / "0.6.0" | Evita importar un segundo Zod para tus herramientas. |
type ToolSpec = {
name: string;
description: string;
inputSchema: ZodType;
readOnly?: boolean;
execute: (
input: any,
context: { signal: AbortSignal }
) => unknown | Promise<unknown>;
};
// registerTool: options = { signal?: AbortSignal }
// registerServerTool: options = {
// signal?: AbortSignal,
// onResult?: (result: any) => unknown
// }
// invokeServer: options = { signal?: AbortSignal }Usa esquemas Zod convertibles a JSON Schema: el SDK llama a toJSONSchema() antes del registro y a parse() antes de ejecutar. Un esquema Zod aceptado por JavaScript puede fallar al convertirse, por ejemplo si incluye transformaciones no representables. readOnly se convierte en una anotación, no en un permiso.
Herramientas JS: ^[a-z][a-z0-9_]{0,99}$. Tipos de componente: ^[a-z][a-z0-9_]{0,29}$. El nombre final con prefijo también debe caber en 100 caracteres. Nombres duplicados se rechazan; no registres un mismo puente global dentro de cada instancia.
04
Ciclo de vida y cancelación
El callback de montaje recibe { id, root, signal, registerTool }. Busca controles dentro de root, no mediante IDs globales. Usa {signal} en listeners y propaga la señal a peticiones y tareas largas.
- El runtime observa inserciones, retiradas y cambios de
data-fwmcp-component. - Al reconciliar una raíz retirada, cancela sus herramientas y ejecuta su función de limpieza.
- Si después se reinserta, se monta como una instancia nueva. Retirar y reinsertar en el mismo turno sin una reconciliación intermedia puede conservar la instancia.
- Una raíz pertenece a un adaptador. Para sustituir controles internos, reemplaza la raíz completa.
- Otros cambios de atributos usados en tu selector requieren
api.reconcile(). - Un error de montaje se registra en consola; no se reintenta mientras la raíz siga montada. Retírala y reconcilia antes de volver a insertarla.
const api = window.FastWebMCP;
const lifetime = new AbortController();
const registered = await api.registerTool({
name: 'mytheme_component_count',
description: 'Read the number of mounted frontend components.',
inputSchema: api.z.object({}).strict(),
readOnly: true,
execute: () => ({ count: api.components().length })
}, { signal: lifetime.signal });
// Al desmontar tu integración:
lifetime.abort();Este fragmento usa await de nivel superior: ejecútalo en un módulo o una función async después de que la API esté disponible. Sin una señal, la herramienta global permanece durante la vida de la página. La señal del executor combina la cancelación de la llamada con la de su registro; no cancela mágicamente trabajo que ignore esa señal.
05
Operaciones PHP extensibles
Registra en init, antes de rest_api_init. Usa un nombre único y un esquema raíz de tipo object con additionalProperties:false. Este ejemplo público solo devuelve una etiqueta saneada; no modifica WordPress.
<?php
add_action('init', function () {
if (!function_exists('fwmcp_register_frontend_tool')) {
return;
}
fwmcp_register_frontend_tool('theme_echo', array(
'description' => 'Echo a short label in the visible theme.',
'public' => true,
'readOnly' => true,
'schema' => array(
'type' => 'object',
'additionalProperties' => false,
'properties' => array(
'label' => array('type' => 'string', 'maxLength' => 80)
),
'required' => array('label')
),
'permission_callback' => '__return_true',
'callback' => function ($input, $request) {
return array('label' => sanitize_text_field($input['label']));
}
));
});<!-- En el markup del theme -->
<p id="theme-status" role="status"></p>
<!-- En tu script de extensión, después de que exista la API -->
<script>
async function setupEcho(api) {
const lifetime = new AbortController();
const output = document.querySelector('#theme-status');
if (!output) return;
try {
await api.registerServerTool('theme_echo', {
signal: lifetime.signal,
onResult: result => { output.textContent = result.label; }
});
} catch (error) {
output.textContent = error.message;
}
// En una integración dinámica, guarda lifetime para llamar abort()
// cuando retires este widget. En navegación completa termina la página.
}
if (window.FastWebMCP) {
void setupEcho(window.FastWebMCP);
} else {
document.addEventListener('fastwebmcp:ready', event => {
void setupEcho(event.detail);
}, { once: true });
}
</script>| Campo | Obligatorio / valor inicial | Contrato |
|---|---|---|
description | Obligatorio | String no vacío; describe entradas, efecto y alcance. |
schema | Obligatorio | Array PHP con JSON Schema raíz estricto. |
callback | Obligatorio | Callable: recibe ($input, $request); devuelve datos, WP_Error o respuesta REST. |
permission_callback | Obligatorio | Callable con WP_REST_Request; devuelve bool o WP_Error. |
public | false | Cuando es false se exige sesión. true no anula capability ni permission_callback. |
capability | String vacío | Capacidad adicional comprobada con current_user_can. |
readOnly | false | Anotación publicada para el agente; no restringe el callback. |
Nombres PHP: ^[a-z][a-z0-9_.-]{0,59}$. El puente genera server_NAME y sustituye puntos/guiones por guiones bajos: theme.echo y theme_echo colisionan. Registrar demasiado tarde lanza LogicException; especificaciones o nombres inválidos/duplicados lanzan InvalidArgumentException.
06
REST, sesión y caché
| Petición | Contrato | Resultado |
|---|---|---|
GET /wp-json/fastwebmcp/v1/frontend-tools | Descubre operaciones para la sesión | {version:1, tools:[{name,description,inputSchema,readOnly}]} |
POST /wp-json/fastwebmcp/v1/frontend-tools/NAME | Body: {"input":{...}} | Datos devueltos por el callback; no añade un wrapper success. |
GET /wp-admin/admin-ajax.php?action=fwmcp_frontend_session | Bootstrap de sesión same-origin sin caché | {success:true,data:{nonce,authenticated}}; nonce vacío si no hay sesión. |
Las URLs REST de la tabla muestran el formato habitual con enlaces permanentes. Usa rest_url() o invokeServer() en integraciones para respetar la configuración real, subdirectorios y formato de rutas. WordPress también admite el parámetro rest_route cuando no usa ese formato. Referencia de rutas WordPress.
const result = await window.FastWebMCP.invokeServer('filter_content', {
query: 'diseño',
type: 'post',
category: 0,
limit: 6
});
// result = { query, category, type, results: [{ id, title, url }] }curl -X POST 'https://example.com/wp-json/fastwebmcp/v1/frontend-tools/filter_content' \
-H 'Content-Type: application/json' \
--data '{"input":{"query":"diseño","type":"post","category":0,"limit":6}}'Reemplaza example.com por un WordPress con el plugin activo. La petición curl verifica REST; no demuestra registro ni ejecución nativa WebMCP.
Autenticación del runtime
Obtiene el nonce mediante Ajax con cookies same-origin y lo envía como X-WP-Nonce. Los scripts frontend no incrustan un nonce de usuario en HTML cacheable. La sesión se mantiene en memoria durante la página. Ante rest_cookie_invalid_nonce, la llamada falla y se invalida ese bootstrap; la siguiente llamada pide una sesión nueva. No hay reintento automático de escrituras.
Respeta respuestas sin caché en el CDN para la sesión y el manifiesto, y evita cachear operaciones autenticadas. Si cambias de cuenta sin recargar la página, redescubre tras recargar para renovar sesión, permisos y herramientas.
07
Permisos, saneamiento y escrituras
El manifiesto filtra por sesión y capability. La comprobación específica de permission_callback ocurre al ejecutar: una operación descubierta puede seguir rechazando una entrada. El plugin usa validación/saneamiento de esquema de WordPress para el argumento input, además de la comprobación de permisos.
// Dentro del spec de una operación privada con schema post_id:
'public' => false,
'capability' => 'edit_posts',
'permission_callback' => function ($request) {
$input = $request->get_param('input');
$id = isset($input['post_id']) ? absint($input['post_id']) : 0;
return $id > 0 && current_user_can('edit_post', $id);
},Es un fragmento de spec, no un registro completo. Declara post_id como integer requerido con mínimo 1 en el esquema de tu operación. Para un recurso diferente, utiliza su capacidad correspondiente y verifica propiedad/alcance según el caso.
- Validar tipos y rangos no sustituye el saneamiento de negocio: usa, por ejemplo,
sanitize_text_fieldowp_kses_postcuando corresponda. - No confíes en ocultar una herramienta ni en
readOnlypara autorizar acceso. REST debe denegar peticiones directas no autorizadas. - Para escrituras propias, implementa preparación, revisión y confirmación con estado de servidor. La API de extensiones no añade ese protocolo automáticamente.
- Una revisión optimista no constituye por sí sola una transacción ni elimina todas las carreras. Evalúa atomicidad, reintentos e idempotencia del caso de uso.
- Usa
textContentpara resultados de texto en la interfaz y valida URLs antes de convertirlas en enlaces.
Modelo WordPress: permission_callback y endpoints personalizados. Las decisiones de permisos por objeto pertenecen a tu extensión.
08
Subconjunto de JSON Schema
| Tipo / clave | Admitido por el puente |
|---|---|
object | properties, required y additionalProperties:false; también para objetos anidados. |
string | minLength, maxLength. |
number / integer | minimum, maximum; integer impone valor entero. |
boolean | Tipo boolean. |
array | items, minItems, maxItems. |
| Comunes | type, description, enum. |
El puente rechaza keywords no compatibles, incluidas pattern, format, $ref, oneOf, anyOf, allOf, default, $schema y tipos nullable en arrays de tipos. Usa este subconjunto para descriptores destinados a registerServerTool.
El registro PHP comprueba el contrato raíz; la conversión del puente se realiza al registrar en el navegador. Que PHP acepte un esquema no garantiza que el puente lo pueda convertir. Para necesidades más amplias registra una herramienta JS con Zod convertible a JSON Schema y mantén validación equivalente en tu operación REST.
{
"type": "object",
"additionalProperties": false,
"properties": {
"query": { "type": "string", "maxLength": 200 },
"type": { "type": "string", "enum": ["post", "page"] },
"category": { "type": "integer", "minimum": 0 },
"limit": { "type": "integer", "minimum": 1, "maximum": 12 }
},
"required": ["query", "type", "category", "limit"]
}09
Bloques, herramientas y flujos incluidos
| Instancia | Herramientas | Flujo |
|---|---|---|
search_N | state({}), prepare({query}), submit({revision}) | prepare rellena y devuelve revisión. submit exige revisión válida, valida el formulario y programa requestSubmit a 300 ms. |
filter_N | state({}), prepare({query,category}), apply({revision}) | apply invoca filter_content y renderiza resultados dentro de esa instancia. |
| Puente de servidor | server_NAME | registerServerTool descubre schema, valida con Zod y ejecuta REST. |
N es un número asignado durante el montaje. Editar manualmente un campo invalida la preparación anterior. Los filtros cancelan búsquedas en curso cuando cambian sus campos. Después de una navegación completa, vuelve a descubrir las herramientas.
Bloque fastwebmcp/content-filter
Atributos: label (por defecto «Explorar contenido»), type (post o page, post por defecto) y limit (6 por defecto; PHP lo limita a 1–12). Solo consulta contenido publicado sin contraseña. En la UI de entradas, prepare permite categorías presentes en el select; en páginas exige category:0.
Sin JavaScript, GET usa fwmcp_query y fwmcp_category, compartidos por las instancias de la página. Con JavaScript, cada bloque conserva campos y resultados propios; con WebMCP ausente/desactivado, siguen disponibles los controles manuales.
Un bloque personalizado
El render PHP de un bloque dinámico puede llamar fwmcp_enqueue_frontend(), emitir una raíz con data-fwmcp-component y cargar su adaptador como un script clásico dependiente de fwmcp-runtime. Usa viewScript y su archivo de assets para el JavaScript frontend de tu bloque. Los módulos ESM pueden consumir la API global una vez disponible; no hay export ESM público del runtime en esta versión.
Campos y carga de assets de bloques: referencia oficial de block.json.
Herramientas administrativas
El editor, biblioteca de medios, panel FastWebMCP y pantallas de ajustes/gestión registran sus propias herramientas según contexto y capacidades. No existe un catálogo universal invariable para toda página. Consulta readme.txt y los adaptadores editor.ts, media-library.ts y admin-tools.ts para nombres y flujos concretos.
10
Eventos, diagnóstico y errores
const api = window.FastWebMCP;
console.log({ version: api?.version, supported: api?.supported() });
console.table(api?.components() ?? []);
document.addEventListener('fastwebmcp:error', event => {
const { name, error } = event.detail;
console.error('WebMCP executor:', name, error);
});fastwebmcp:ready se emite en document con la API en detail. fastwebmcp:error informa errores capturados dentro del executor. La validación Zod anterior al executor y los fallos de registro no pasan por este evento: maneja también las Promises de registro y los errores que recibe el agente.
| Síntoma | Comprobar |
|---|---|
| window.FastWebMCP no existe | Que el render/helper haya cargado fwmcp-runtime; orden/dependencias del script y errores de consola. |
| supported() devuelve false | Filtro frontend_enabled, presencia de document.modelContext y compatibilidad del navegador/agente. |
| Nombre inválido o duplicado | Prefijo final, longitud, nombres globales y conversión . / - a _ en puentes. |
| Operación no disponible para esta sesión | Registro PHP anterior a rest_api_init, sesión y capacidad del manifiesto. |
| Revisión desactualizada | Prepara de nuevo después de editar manualmente o navegar; no reutilices revisiones entre instancias. |
| Esquema JSON no compatible | Keywords fuera del subconjunto, additionalProperties o required inválidos. |
| REST deniega acceso | Nonce y cookies, capability y permission_callback; la visibilidad del descriptor no concede permiso. |
| Resultados manuales fallan detrás de caché | Rutas same-origin, nonce vigente y exclusión de respuestas autenticadas del caché. |
REST puede responder 400 por entradas inválidas, 401/403 por acceso o autenticación, y 404 por una operación inexistente. Tus callbacks pueden devolver otros estados mediante WP_Error/REST. invokeServer transforma errores HTTP en Error(message); no conserva status/code como propiedades públicas del Error. Para diagnosticar esos datos usa la pestaña Network.
// Desactivar únicamente el registro WebMCP del frontend:
add_filter('fwmcp_frontend_enabled', '__return_false');
// Mostrar el panel de laboratorio opcional en el frontend:
// El plugin exige además manage_options para mostrarlo/cargarlo.
add_filter('fwmcp_frontend_debug_panel', '__return_true');11
Compilación, pruebas y distribución
git clone https://github.com/MauricioPerera/fastwebmcp-wordpress.git
cd fastwebmcp-wordpress
npm ci
npm run typecheck
npm run build
npm test
npm run packageDesarrollo: Node.js 22+ y Python 3 para el empaquetado. PHP 8+ permite comprobar sintaxis localmente. Las dependencias están fijadas en package-lock.json: Zod 4.5.4, esbuild 0.28.2 y TypeScript 7.0.2. vendor/fastwebmcp incluye el subconjunto MIT de registro imperativo del SDK; no requiere un checkout adyacente.
npm run build: genera los siete bundles IIFE de assets/.npm test: prueba el runtime compilado en VM con un mock de document.modelContext; esquemas, duplicados, cancelación, retirada/remontaje, disposal y ausencia de WebMCP.npm run package: genera dist/fastwebmcp-wordpress.zip, con carpeta raíz fastwebmcp-wordpress, assets, bloques, fuentes y licencias.- CI comprueba tipos, compilación, pruebas, sincronización de bundles, sintaxis PHP y ZIP. Los datos y fixtures WordPress del laboratorio externo no se distribuyen.
La VM valida el contrato local del runtime, no la integración del navegador. La versión 0.6.0 se validó también con WebMCP real en un WordPress local: dos búsquedas, dos filtros, retirada/remontaje, extensión PHP, rechazos y uso manual; 27 comprobaciones REST nuevas y 105 de regresión en ese laboratorio.
Cuando cambies fuentes TypeScript, reconstruye assets y ejecuta pruebas antes de empaquetar. La instalación del ZIP compilado no requiere Node, Python ni descargar scripts remotos.
12
Límites y referencias
- Runtime compartido: aproximadamente 429 KiB sin compresión; adaptadores frontend, 4.2 KiB; editor del filtro, menos de 1 KiB. No se ha optimizado aún el tamaño de Zod.
- La navegación dinámica admite sustituir raíces completas, no incluye un router SPA ni garantiza compatibilidad con cualquier theme, CDN o plugin.
- Los esquemas tienen el subconjunto indicado; el protocolo de preparación/revisión de una extensión debe implementarse expresamente.
- El comportamiento está documentado para 0.6.0. Consulta la release y el código antes de asumir compatibilidad con versiones posteriores.