Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/assets/js/webmcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===
Expand All @@ -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';
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down
40 changes: 35 additions & 5 deletions src/webmcp/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ webmcp: true
<section class="panel" aria-labelledby="webmcp-title">
<p class="eyebrow">WebMCP Challenge · live public surface</p>
<h1 id="webmcp-title">The Interdependency WebMCP</h1>
<p><strong>The page is the provider.</strong> Humans can operate the registry here; WebMCP-capable browsers receive the same five tools through <code>document.modelContext.registerTool(...)</code>; remote MCP clients connect to the website-owned Render runtime.</p>
<p><strong>The page is the provider.</strong> Humans can browse and operate the skill registry here; WebMCP-capable browsers receive the same five operations through <code>document.modelContext.registerTool(...)</code>; remote MCP clients connect to the website-owned Render runtime.</p>
<p><strong>Remote MCP:</strong> <code>https://the-interdependency-mcp.onrender.com/mcp</code></p>
<p><a href="https://the-interdependency-mcp.onrender.com/health">Open remote MCP health check</a></p>
</section>
Expand All @@ -18,12 +18,41 @@ webmcp: true
<p data-webmcp-status data-state="hmmm">Loading the commit-pinned registry and checking browser WebMCP support…</p>
<p data-remote-mcp-status data-state="hmmm">Checking remote MCP health…</p>
<p><strong>Registry source:</strong> <span data-webmcp-source>resolving…</span></p>
<noscript><p>JavaScript is disabled, so browser WebMCP registration and the human controls below cannot run. The remote MCP endpoint remains independent.</p></noscript>
<noscript><p>JavaScript is disabled, so browser WebMCP registration and the interactive controls cannot run. The human-readable catalogue below remains available, and the remote MCP endpoint remains independent.</p></noscript>
</section>

<section class="panel" aria-labelledby="skill-catalog-title">
<p class="eyebrow">Human surface · {{ generated.skillRegistry.skills.length }} skills</p>
<h2 id="skill-catalog-title">Skill catalogue</h2>
<p>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.</p>

<form data-human-skill-filter-form>
<label for="human-skill-filter">Search skills</label>
<input id="human-skill-filter" data-human-skill-filter name="skill-filter" type="search" placeholder="repository audit, UCNS, metadata, SSH…" autocomplete="off">
<span aria-live="polite" data-human-skill-count>{{ generated.skillRegistry.skills.length }} skills shown</span>
</form>

<div class="grid" data-human-skill-catalog>
{% for skill in generated.skillRegistry.skills %}
<article class="card" data-human-skill>
<p class="eyebrow">{{ skill.kind }}</p>
<h3>{{ skill.name | replace("-", " ") }}</h3>
<p><code>{{ skill.name }}</code></p>
<p>{{ skill.description }}</p>
{% if skill.depends_on and skill.depends_on.length %}
<p><strong>Depends on:</strong> {% for dependency in skill.depends_on %}<code>{{ dependency }}</code>{% if not loop.last %}, {% endif %}{% endfor %}</p>
{% else %}
<p><strong>Depends on:</strong> none declared</p>
{% endif %}
<p><a href="https://github.com/{{ generated.skillRegistry.source.repository }}/blob/{{ generated.skillRegistry.source.commit }}/{{ skill.path }}">Read canonical SKILL.md</a></p>
</article>
{% endfor %}
</div>
</section>

<section class="panel" aria-labelledby="webmcp-tools-title">
<h2 id="webmcp-tools-title">Five registry operations</h2>
<p>The controls below execute the same read-only registry logic exposed to agents.</p>
<p>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.</p>

<p>
<button type="button" data-webmcp-action="status">STATUS</button>
Expand Down Expand Up @@ -52,13 +81,14 @@ webmcp: true
</section>

<section class="panel" aria-labelledby="webmcp-boundary-title">
<h2 id="webmcp-boundary-title">One source, two agent surfaces</h2>
<h2 id="webmcp-boundary-title">One source, three views</h2>
<pre tabindex="0">skill-lib/SKILL.md + skills.json
↓ commit-pinned projection
website registry logic
├── human-readable skill catalogue
├── browser WebMCP provider
└── remote Streamable HTTP MCP server</pre>
<p>The website owns both delivery surfaces. <code>The-Interdependency/skill-lib</code> remains authority for the skills themselves. All v0 operations are read-only.</p>
<p>The website owns the delivery surfaces. <code>The-Interdependency/skill-lib</code> remains authority for the skills themselves. All v0 operations are read-only.</p>
</section>

<div class="hmmm-boundary"><strong>hmmm</strong><span>Write-capable operations remain intentionally absent. Installing, propagating, or changing repository state requires a separate authenticated boundary rather than silently enlarging this public surface.</span></div>
19 changes: 15 additions & 4 deletions tests/webmcp.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'),
Expand All @@ -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/);
Expand Down
Loading