diff --git a/src/assets/js/webmcp.js b/src/assets/js/webmcp.js
index 42e4ec3..2000a53 100644
--- a/src/assets/js/webmcp.js
+++ b/src/assets/js/webmcp.js
@@ -2,7 +2,7 @@ import { createSkillRegistry } from './webmcp-registry.js';
// === MODULE_BUILD ===
// id: interdependency_webmcp_surface
-// purpose: Register the website-owned, read-only WebMCP tool surface and provide the same registry operations to the human-facing demo page.
+// purpose: Register the website-owned, read-only WebMCP tool surface, provide the same registry operations to the human-facing demo page, and filter the build-time human skill catalogue.
// entrypoint: /webmcp/
// tests: tests/webmcp.test.mjs
// === END MODULE_BUILD ===
@@ -19,8 +19,13 @@ import { createSkillRegistry } from './webmcp-registry.js';
// given: a human control or browser agent invokes any v0 operation
// then: execution reads the generated registry projection and returns structured results without mutating the site, GitHub, or skill-lib
// class: safety
+//
+// id: webmcp_human_catalogue_remains_source_bound
+// given: a visitor browses or filters the human-readable skill catalogue
+// then: filtering only hides or reveals build-time cards derived from the same generated registry and never creates a second skill definition
+// class: correctness
// === END CONTRACTS ===
-// Usage: open `/webmcp/` in any browser to exercise the five registry operations directly. In a WebMCP-capable browser the same operations register through `document.modelContext.registerTool(...)`. The remote MCP endpoint is independently health-checked and remains read-only.
+// Usage: open `/webmcp/` in any browser to browse the human-readable skill catalogue and exercise the five registry operations directly. In a WebMCP-capable browser the same operations register through `document.modelContext.registerTool(...)`. The remote MCP endpoint is independently health-checked and remains read-only.
const REGISTRY_URL = '/assets/data/skill-registry.json';
const REMOTE_MCP_BASE = 'https://the-interdependency-mcp.onrender.com';
@@ -81,6 +86,30 @@ async function checkRemoteMcp() {
}
}
+function bindHumanCatalogue() {
+ const form = document.querySelector('[data-human-skill-filter-form]');
+ const input = document.querySelector('[data-human-skill-filter]');
+ const count = document.querySelector('[data-human-skill-count]');
+ const cards = [...document.querySelectorAll('[data-human-skill]')];
+
+ form?.addEventListener('submit', event => event.preventDefault());
+ if (!input || cards.length === 0) return;
+
+ const applyFilter = () => {
+ const query = String(input.value || '').trim().toLowerCase();
+ let visible = 0;
+ for (const card of cards) {
+ const show = !query || card.textContent.toLowerCase().includes(query);
+ card.hidden = !show;
+ if (show) visible += 1;
+ }
+ if (count) count.textContent = `${visible} of ${cards.length} skills shown`;
+ };
+
+ input.addEventListener('input', applyFilter);
+ applyFilter();
+}
+
function bindHumanControls(registry) {
document.querySelector('[data-webmcp-action="status"]')?.addEventListener('click', () => {
showResult('STATUS', registry.getRegistryStatus());
@@ -128,11 +157,12 @@ export async function registerInterdependencyWebMCP() {
const registry = createSkillRegistry(data);
const status = registry.getRegistryStatus();
updateSource(status);
+ bindHumanCatalogue();
bindHumanControls(registry);
void checkRemoteMcp();
if (!modelContext()?.registerTool) {
- setStatus(`Registry live for ${status.skill_count} skills. Browser WebMCP registration requires a WebMCP-capable browser; the human controls and remote MCP server remain usable.`, 'hmmm');
+ setStatus(`Registry live for ${status.skill_count} skills. Browser WebMCP registration requires a WebMCP-capable browser; the human catalogue, controls, and remote MCP server remain usable.`, 'hmmm');
return { registered: false, reason: 'webmcp-unavailable', registry: status };
}
if (globalThis.__interdependencyWebMcpRegistered) {
diff --git a/src/webmcp/index.njk b/src/webmcp/index.njk
index d864ad6..ff4daae 100644
--- a/src/webmcp/index.njk
+++ b/src/webmcp/index.njk
@@ -8,7 +8,7 @@ webmcp: true
WebMCP Challenge · live public surface
The Interdependency WebMCP
-
The page is the provider. Humans can operate the registry here; WebMCP-capable browsers receive the same five tools through document.modelContext.registerTool(...); remote MCP clients connect to the website-owned Render runtime.
+
The page is the provider. Humans can browse and operate the skill registry here; WebMCP-capable browsers receive the same five operations through document.modelContext.registerTool(...); remote MCP clients connect to the website-owned Render runtime.
Loading the commit-pinned registry and checking browser WebMCP support…
Checking remote MCP health…
Registry source:resolving…
-
+
+
+
+
+
Human surface · {{ generated.skillRegistry.skills.length }} skills
+
Skill catalogue
+
These are the skills exposed by the same commit-pinned registry used by WebMCP and the remote MCP server. The description shown here is the registry's human-readable statement of when and why each skill is used.
+
+
+
+
+ {% for skill in generated.skillRegistry.skills %}
+
+
{{ skill.kind }}
+
{{ skill.name | replace("-", " ") }}
+
{{ skill.name }}
+
{{ skill.description }}
+ {% if skill.depends_on and skill.depends_on.length %}
+
Depends on: {% for dependency in skill.depends_on %}{{ dependency }}{% if not loop.last %}, {% endif %}{% endfor %}
The controls below execute the same read-only registry logic exposed to agents.
+
These controls execute the same read-only registry logic exposed to browser agents and remote MCP clients. The raw output is intentionally shown here as the machine-facing view of the same catalogue above.
The website owns both delivery surfaces. The-Interdependency/skill-lib remains authority for the skills themselves. All v0 operations are read-only.
+
The website owns the delivery surfaces. The-Interdependency/skill-lib remains authority for the skills themselves. All v0 operations are read-only.
hmmmWrite-capable operations remain intentionally absent. Installing, propagating, or changing repository state requires a separate authenticated boundary rather than silently enlarging this public surface.
diff --git a/tests/webmcp.test.mjs b/tests/webmcp.test.mjs
index a1f239a..e17e612 100644
--- a/tests/webmcp.test.mjs
+++ b/tests/webmcp.test.mjs
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises';
import { normalizeRegistry, readFallback } from '../scripts/fetch-skill-registry.mjs';
import { createSkillRegistry } from '../src/assets/js/webmcp-registry.js';
-// Usage: run with `npm test`; these checks verify registry provenance, dependency closure, clean-checkout fallback identity, current WebMCP Document API usage, live human controls, and the visible remote MCP endpoint without requiring a WebMCP-capable test browser.
+// Usage: run with `npm test`; these checks verify registry provenance, dependency closure, clean-checkout fallback identity, current WebMCP Document API usage, the build-time human-readable skill catalogue, live human controls, and the visible remote MCP endpoint without requiring a WebMCP-capable test browser.
const BOOTSTRAP_SNAPSHOT_COMMIT = '260671303733a45c8f8d5563e41d8854e09856e6';
const SNAPSHOT_PATH = 'src/_data/snapshots/skill-registry.last-known-good.json';
@@ -90,18 +90,20 @@ test('WebMCP provider registers only the five declared read-only registry tools
assert.doesNotMatch(source, /unregisterTool|install_skill|propagate_skill|update_file|create_file/);
});
-test('registry provenance and human controls resolve before unsupported-browser WebMCP return', async () => {
+test('human catalogue and controls resolve before unsupported-browser WebMCP return', async () => {
const source = await readFile('src/assets/js/webmcp.js', 'utf8');
const loadIndex = source.indexOf('const data = await loadRegistry();');
const sourceIndex = source.indexOf('updateSource(status);');
+ const catalogueIndex = source.indexOf('bindHumanCatalogue();');
const controlsIndex = source.indexOf('bindHumanControls(registry);');
const unsupportedIndex = source.indexOf("if (!modelContext()?.registerTool)");
assert.ok(loadIndex >= 0 && sourceIndex > loadIndex);
- assert.ok(controlsIndex > sourceIndex);
+ assert.ok(catalogueIndex > sourceIndex);
+ assert.ok(controlsIndex > catalogueIndex);
assert.ok(unsupportedIndex > controlsIndex);
});
-test('dedicated WebMCP route is the visible browser and remote MCP front door', async () => {
+test('dedicated WebMCP route is a human-readable skill browser and the agent front door', async () => {
const [page, layout, packageJson] = await Promise.all([
readFile('src/webmcp/index.njk', 'utf8'),
readFile('src/_includes/layouts/base.njk', 'utf8'),
@@ -114,6 +116,15 @@ test('dedicated WebMCP route is the visible browser and remote MCP front door',
assert.match(page, /document\.modelContext\.registerTool/);
assert.match(page, /https:\/\/the-interdependency-mcp\.onrender\.com\/mcp/);
assert.match(page, /data-remote-mcp-status/);
+
+ assert.match(page, /id="skill-catalog-title">Skill catalogue/);
+ assert.match(page, /generated\.skillRegistry\.skills/);
+ assert.match(page, /data-human-skill-filter/);
+ assert.match(page, /data-human-skill-catalog/);
+ assert.match(page, /data-human-skill/);
+ assert.match(page, /skill\.description/);
+ assert.match(page, /Read canonical SKILL\.md/);
+
assert.match(page, /data-webmcp-action="status"/);
assert.match(page, /data-webmcp-find/);
assert.match(page, /data-webmcp-inspect/);