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
42 changes: 31 additions & 11 deletions src/assets/js/webmcp-registry.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// === MODULE_BUILD ===
// id: webmcp_skill_registry_adapter
// purpose: Provide deterministic, read-only operations over the website's commit-pinned skill-lib registry projection.
// entrypoint: imported by /assets/js/webmcp.js and tests/webmcp.test.mjs
// tests: tests/webmcp.test.mjs
// purpose: Provide deterministic read-only operations over the public human-and-agent view of the website's commit-pinned skill-lib registry projection.
// entrypoint: imported by /assets/js/webmcp.js, server/mcp-protocol.mjs, and tests
// tests: tests/webmcp.test.mjs, tests/mcp-server.test.mjs
// === END MODULE_BUILD ===
// === BOUNDARIES ===
// id: webmcp_skill_registry_read_boundary
Expand All @@ -12,17 +12,26 @@
// operational_effects: none; all exported operations are read-only transformations over supplied registry data
// === END BOUNDARIES ===
// === CONTRACTS ===
// id: webmcp_public_catalogue_matches_human_catalogue
// given: the source registry contains public-facing and internal/specialist skills
// then: public list/find/inspect/closure expose only metadata-block msdmd applications plus the exact meta skill, matching the human card catalogue
// class: correctness
//
// id: webmcp_registry_smallest_dependency_closure
// given: a registered skill name
// then: resolveSkillClosure returns that skill plus every transitive depends_on prerequisite exactly once in dependency-first order
// given: a presented registered skill name
// then: resolveSkillClosure returns that skill plus every presented transitive depends_on prerequisite exactly once in dependency-first order
// class: correctness
// === END CONTRACTS ===
// Usage: `const registry = createSkillRegistry(data); registry.findSkills({ query: 'repository audit' })`. Consumers must pass the generated `/assets/data/skill-registry.json` projection, never invent skill records locally.
// Usage: `const registry = createSkillRegistry(data); registry.findSkills({ query: 'documentation' })`. Consumers pass the generated registry projection; the adapter derives one shared public view rather than inventing separate human and agent registries.

function normalizeText(value) {
return String(value || '').trim().toLowerCase();
}

function isPresentedSkill(skill) {
return skill?.kind === 'metadata-block' || skill?.name === 'meta';
}

function publicSkill(skill, source) {
return {
name: skill.name,
Expand All @@ -40,17 +49,26 @@ export function createSkillRegistry(registryData) {
}

const source = registryData.source;
const byName = new Map(registryData.skills.map(skill => [skill.name, skill]));
const skills = registryData.skills.filter(isPresentedSkill);
const byName = new Map(skills.map(skill => [skill.name, skill]));

for (const skill of skills) {
for (const dependency of skill.depends_on) {
if (!byName.has(dependency)) {
throw new Error(`presented skill dependency is not presented: ${skill.name} -> ${dependency}`);
Comment on lines +55 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept valid dependencies outside the presented subset

If a future metadata-block skill declares a dependency on a procedural skill other than meta, the source projection remains valid under normalizeRegistry, but this new check makes createSkillRegistry throw. Because both the browser page and remote server construct this adapter at startup, one valid cross-scope dependency from the automatically refreshed upstream registry would disable the entire public surface; either include such prerequisites when resolving closures or enforce the restriction during refresh so the verified fallback can be used.

Useful? React with 👍 / 👎.

}
}
}

function requireSkill(name) {
const skill = byName.get(String(name || '').trim());
if (!skill) throw new Error(`unknown skill: ${name}`);
if (!skill) throw new Error(`unknown public skill: ${name}`);
return skill;
}

function listSkills({ kind = '' } = {}) {
const normalizedKind = normalizeText(kind);
return registryData.skills
return skills
.filter(skill => !normalizedKind || normalizeText(skill.kind) === normalizedKind)
.map(skill => publicSkill(skill, source));
}
Expand All @@ -61,7 +79,7 @@ export function createSkillRegistry(registryData) {
const boundedLimit = Math.max(1, Math.min(Number(limit) || 8, 20));
if (terms.length === 0) return listSkills({ kind }).slice(0, boundedLimit);

return registryData.skills
return skills
.filter(skill => !normalizedKind || normalizeText(skill.kind) === normalizedKind)
.map(skill => {
const name = normalizeText(skill.name);
Expand Down Expand Up @@ -109,7 +127,9 @@ export function createSkillRegistry(registryData) {
function getRegistryStatus() {
return {
registry_version: registryData.version,
skill_count: registryData.skills.length,
skill_count: skills.length,
source_skill_count: registryData.skills.length,
public_scope: 'metadata-block plus meta',
source: { ...source },
fallback: Boolean(registryData.fallback),
hmmm: Array.isArray(registryData.hmmm) ? [...registryData.hmmm] : []
Expand Down
105 changes: 61 additions & 44 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, provide the same registry operations to the human-facing demo page, and filter the build-time human skill catalogue.
// purpose: Register the website-owned read-only WebMCP tools and bind the human-readable skill-selection surface to the same commit-pinned registry records.
// entrypoint: /webmcp/
// tests: tests/webmcp.test.mjs
// === END MODULE_BUILD ===
Expand All @@ -11,28 +11,29 @@ import { createSkillRegistry } from './webmcp-registry.js';
// network: same-origin GET of /assets/data/skill-registry.json plus read-only health GET to the website-owned Render MCP runtime
// storage: none
// user_data: none
// operational_effects: none; v0 exposes registry discovery and dependency resolution only
// operational_effects: none; selection changes only page state and URL query, while v0 MCP operations remain read-only
// authority: the website owns tool registration and remote runtime; The-Interdependency/skill-lib remains authority for skill definitions
// === END BOUNDARIES ===
// === CONTRACTS ===
// id: webmcp_tools_are_read_only_registry_operations
// given: a human control or browser agent invokes any v0 operation
// given: a 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
// id: webmcp_human_selection_is_exact_registry_identity
// given: a human selects a presented skill card
// then: the page opens that card's description, records the exact registered skill name in visible state and the URL, and derives inspection/closure from the same registry object without requiring typed internal identifiers
// class: correctness
// === END CONTRACTS ===
// 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.
// Usage: open `/webmcp/`; humans select from the curated cards while agents receive the same canonical registry material through `document.modelContext.registerTool(...)`. Selection is instruction, not write authority.

const REGISTRY_URL = '/assets/data/skill-registry.json';
const REMOTE_MCP_BASE = 'https://the-interdependency-mcp.onrender.com';
const statusElement = () => document.querySelector('[data-webmcp-status]');
const sourceElement = () => document.querySelector('[data-webmcp-source]');
const remoteStatusElement = () => document.querySelector('[data-remote-mcp-status]');
const outputElement = () => document.querySelector('[data-webmcp-output]');
const selectedElement = () => document.querySelector('[data-selected-skill]');
const modelContext = () => globalThis.document?.modelContext;

function setStatus(message, state = 'hmmm') {
Expand Down Expand Up @@ -86,64 +87,81 @@ async function checkRemoteMcp() {
}
}

function bindHumanCatalogue() {
function bindHumanCatalogue(registry) {
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]')];
const selectedActions = [...document.querySelectorAll('[data-selected-action]')];
let selectedName = '';

form?.addEventListener('submit', event => event.preventDefault());
if (!input || cards.length === 0) return;

const applyFilter = () => {
const query = String(input.value || '').trim().toLowerCase();
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`;
if (count) count.textContent = `${visible} of ${cards.length} presented skills shown`;
};

input.addEventListener('input', applyFilter);
applyFilter();
}
const setSelected = (name, { updateUrl = true } = {}) => {
const card = cards.find(candidate => candidate.dataset.skillName === name);
if (!card) return false;

function bindHumanControls(registry) {
document.querySelector('[data-webmcp-action="status"]')?.addEventListener('click', () => {
showResult('STATUS', registry.getRegistryStatus());
});
const skill = registry.inspectSkill({ name });
selectedName = name;

document.querySelector('[data-webmcp-action="list"]')?.addEventListener('click', () => {
showResult('LIST', registry.listSkills());
});
for (const candidate of cards) {
const selected = candidate === card;
candidate.dataset.selected = selected ? 'true' : 'false';
candidate.querySelector('[data-select-skill]')?.setAttribute('aria-pressed', String(selected));
if (selected) candidate.querySelector('[data-skill-description]')?.setAttribute('open', '');
}

document.querySelector('[data-webmcp-find]')?.addEventListener('submit', event => {
event.preventDefault();
const query = String(new FormData(event.currentTarget).get('query') || '').trim();
showResult('FIND', registry.findSkills({ query }));
});
const target = selectedElement();
if (target) {
target.textContent = `${skill.name} · ${skill.kind} · ${skill.canonical_path}`;
target.dataset.skillName = skill.name;
}

document.querySelector('[data-webmcp-inspect]')?.addEventListener('submit', event => {
event.preventDefault();
const name = String(new FormData(event.currentTarget).get('name') || '').trim();
try {
showResult('INSPECT', registry.inspectSkill({ name }));
} catch (error) {
showResult('INSPECT', { error: error.message });
for (const action of selectedActions) action.disabled = false;
showResult('SELECTED SKILL', skill);

if (updateUrl) {
const url = new URL(globalThis.location.href);
url.searchParams.set('skill', skill.name);
globalThis.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
Comment on lines +136 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate the selection to the remote MCP path

When a visitor uses the advertised remote MCP path, setting ?skill= only modifies that visitor's local page URL and DOM; the remote server is stateless and its inspect/closure tools still require callers to supply an exact name. Consequently the remote agent receives no selected skill identity despite the page depicting the selection as flowing to remote MCP, so this path needs an actual handoff mechanism or must be described as browser-local only.

Useful? React with 👍 / 👎.

}
return true;
};

for (const card of cards) {
card.querySelector('[data-select-skill]')?.addEventListener('click', () => {
setSelected(card.dataset.skillName || '');
});
}

document.querySelector('[data-selected-action="inspect"]')?.addEventListener('click', () => {
if (!selectedName) return;
showResult('INSPECT SELECTED', registry.inspectSkill({ name: selectedName }));
});

document.querySelector('[data-webmcp-closure]')?.addEventListener('submit', event => {
event.preventDefault();
const name = String(new FormData(event.currentTarget).get('name') || '').trim();
try {
showResult('RESOLVE CLOSURE', registry.resolveSkillClosure({ name }));
} catch (error) {
showResult('RESOLVE CLOSURE', { error: error.message });
}
document.querySelector('[data-selected-action="closure"]')?.addEventListener('click', () => {
if (!selectedName) return;
showResult('REQUIRED SKILL SET', registry.resolveSkillClosure({ name: selectedName }));
});

input?.addEventListener('input', applyFilter);
applyFilter();

const requested = new URL(globalThis.location.href).searchParams.get('skill');
if (requested) setSelected(requested, { updateUrl: false });

return { getSelectedName: () => selectedName };
}

async function registerTool(tool) {
Expand All @@ -157,12 +175,11 @@ export async function registerInterdependencyWebMCP() {
const registry = createSkillRegistry(data);
const status = registry.getRegistryStatus();
updateSource(status);
bindHumanCatalogue();
bindHumanControls(registry);
bindHumanCatalogue(registry);
void checkRemoteMcp();

if (!modelContext()?.registerTool) {
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');
setStatus(`Registry live for ${status.skill_count} skills. Browser WebMCP registration requires a WebMCP-capable browser; human selection and the remote MCP server remain usable.`, 'hmmm');
return { registered: false, reason: 'webmcp-unavailable', registry: status };
}
if (globalThis.__interdependencyWebMcpRegistered) {
Expand Down
Loading
Loading