-
Notifications
You must be signed in to change notification settings - Fork 0
WebMCP: make human selection the agent handoff #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ef351f
7d860b7
6c8b402
7b5b3eb
667b0ef
724fd0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 === | ||
|
|
@@ -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') { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a visitor uses the advertised remote MCP path, setting 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) { | ||
|
|
@@ -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) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a future metadata-block skill declares a dependency on a procedural skill other than
meta, the source projection remains valid undernormalizeRegistry, but this new check makescreateSkillRegistrythrow. 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 👍 / 👎.