diff --git a/server/mcp-server.mjs b/server/mcp-server.mjs
index 00021cf..a72b63c 100644
--- a/server/mcp-server.mjs
+++ b/server/mcp-server.mjs
@@ -46,7 +46,7 @@ import {
//
// id: remote_mcp_origin_validation
// given: a request supplies an Origin header
-// then: only an explicitly allowed origin is accepted
+// then: only an explicitly allowed origin is accepted, including for the browser-visible health route
// class: security
//
// id: remote_mcp_registry_source_is_verified_projection
@@ -104,6 +104,13 @@ function isOriginAllowed(request, allowedOrigins) {
return !origin || allowedOrigins.has(origin);
}
+function corsHeaders(request, allowedOrigins) {
+ const origin = request.headers.origin;
+ return origin && allowedOrigins.has(origin)
+ ? { 'access-control-allow-origin': origin }
+ : {};
+}
+
async function readJsonBody(request) {
let size = 0;
const chunks = [];
@@ -174,12 +181,15 @@ export function createInterdependencyMcpServer(registryData, {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (request.method === 'GET' && url.pathname === '/health') {
+ if (!isOriginAllowed(request, allowedOrigins)) {
+ return sendJson(response, 403, rpcError(null, -32000, 'Forbidden origin'));
+ }
return sendJson(response, 200, {
ok: true,
service: 'the-interdependency-mcp',
endpoint: '/mcp',
skill_count: protocol.registry.getRegistryStatus().skill_count
- });
+ }, corsHeaders(request, allowedOrigins));
}
if (url.pathname !== '/mcp') {
@@ -195,9 +205,8 @@ export function createInterdependencyMcpServer(registryData, {
}
if (request.method === 'OPTIONS') {
- const origin = request.headers.origin;
return sendEmpty(response, 204, {
- ...(origin && allowedOrigins.has(origin) ? { 'access-control-allow-origin': origin } : {}),
+ ...corsHeaders(request, allowedOrigins),
'access-control-allow-methods': 'POST, OPTIONS',
'access-control-allow-headers': 'content-type, accept, mcp-protocol-version, mcp-method, mcp-name',
'access-control-max-age': '600'
@@ -236,10 +245,7 @@ export function createInterdependencyMcpServer(registryData, {
const result = protocol.handle(message, { protocolVersion });
if (result?.notification) return sendEmpty(response, 202);
- const origin = request.headers.origin;
- return sendJson(response, 200, result, {
- ...(origin && allowedOrigins.has(origin) ? { 'access-control-allow-origin': origin } : {})
- });
+ return sendJson(response, 200, result, corsHeaders(request, allowedOrigins));
});
}
diff --git a/src/assets/js/webmcp.js b/src/assets/js/webmcp.js
index 164a0eb..42e4ec3 100644
--- a/src/assets/js/webmcp.js
+++ b/src/assets/js/webmcp.js
@@ -2,30 +2,33 @@ import { createSkillRegistry } from './webmcp-registry.js';
// === MODULE_BUILD ===
// id: interdependency_webmcp_surface
-// purpose: Register the website-owned, read-only WebMCP tool surface over the commit-pinned skill-lib registry projection.
+// purpose: Register the website-owned, read-only WebMCP tool surface and provide the same registry operations to the human-facing demo page.
// entrypoint: /webmcp/
// tests: tests/webmcp.test.mjs
// === END MODULE_BUILD ===
// === BOUNDARIES ===
// id: interdependency_webmcp_surface_boundary
-// network: same-origin GET of /assets/data/skill-registry.json only
+// 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
-// authority: the website owns tool registration; The-Interdependency/skill-lib remains authority for skill definitions
+// 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: an agent invokes any v0 tool
+// 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
// === END CONTRACTS ===
-// Usage: open `/webmcp/` in a WebMCP-capable browser or in-app browser. The page registers five read-only tools through `navigator.modelContext`. Unsupported browsers still resolve and display registry provenance without polyfilling or faking WebMCP support.
+// 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.
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 modelContext = () => globalThis.navigator?.modelContext;
+const remoteStatusElement = () => document.querySelector('[data-remote-mcp-status]');
+const outputElement = () => document.querySelector('[data-webmcp-output]');
+const modelContext = () => globalThis.document?.modelContext;
function setStatus(message, state = 'hmmm') {
const target = statusElement();
@@ -34,10 +37,23 @@ function setStatus(message, state = 'hmmm') {
target.dataset.state = state;
}
+function setRemoteStatus(message, state = 'hmmm') {
+ const target = remoteStatusElement();
+ if (!target) return;
+ target.textContent = message;
+ target.dataset.state = state;
+}
+
function jsonResult(value) {
return JSON.stringify(value, null, 2);
}
+function showResult(label, value) {
+ const target = outputElement();
+ if (!target) return;
+ target.textContent = `${label}\n\n${jsonResult(value)}`;
+}
+
async function loadRegistry() {
const response = await fetch(REGISTRY_URL, { headers: { accept: 'application/json' } });
if (!response.ok) throw new Error(`registry HTTP ${response.status}`);
@@ -51,6 +67,56 @@ function updateSource(status) {
target.textContent = `${status.source.repository}@${status.source.commit.slice(0, 12)}:${status.source.path}${suffix}`;
}
+async function checkRemoteMcp() {
+ try {
+ const response = await fetch(`${REMOTE_MCP_BASE}/health`, { headers: { accept: 'application/json' } });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const health = await response.json();
+ if (!health?.ok || health.endpoint !== '/mcp') throw new Error('invalid health response');
+ setRemoteStatus(`Remote MCP LIVE · ${health.skill_count} skills · ${REMOTE_MCP_BASE}/mcp`, 'implemented');
+ return health;
+ } catch (error) {
+ setRemoteStatus(`Remote MCP health unresolved: ${error.message}`, 'hmmm');
+ return null;
+ }
+}
+
+function bindHumanControls(registry) {
+ document.querySelector('[data-webmcp-action="status"]')?.addEventListener('click', () => {
+ showResult('STATUS', registry.getRegistryStatus());
+ });
+
+ document.querySelector('[data-webmcp-action="list"]')?.addEventListener('click', () => {
+ showResult('LIST', registry.listSkills());
+ });
+
+ 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 }));
+ });
+
+ 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 });
+ }
+ });
+
+ 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 });
+ }
+ });
+}
+
async function registerTool(tool) {
const context = modelContext();
if (!context?.registerTool) throw new Error('WebMCP API unavailable during tool registration');
@@ -62,9 +128,11 @@ export async function registerInterdependencyWebMCP() {
const registry = createSkillRegistry(data);
const status = registry.getRegistryStatus();
updateSource(status);
+ bindHumanControls(registry);
+ void checkRemoteMcp();
if (!modelContext()?.registerTool) {
- setStatus(`WebMCP API unavailable in this browser. Registry provenance resolved for ${status.skill_count} skills; tool registration requires a WebMCP-capable browser.`, 'hmmm');
+ 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');
return { registered: false, reason: 'webmcp-unavailable', registry: status };
}
if (globalThis.__interdependencyWebMcpRegistered) {
@@ -142,7 +210,7 @@ export async function registerInterdependencyWebMCP() {
});
globalThis.__interdependencyWebMcpRegistered = true;
- setStatus(`WebMCP live: 5 read-only tools registered over ${status.skill_count} skills.`, status.fallback ? 'hmmm' : 'implemented');
+ setStatus(`WebMCP LIVE · 5 read-only tools registered over ${status.skill_count} skills.`, status.fallback ? 'hmmm' : 'implemented');
return { registered: true, tools: 5, registry: status };
}
diff --git a/src/webmcp/index.njk b/src/webmcp/index.njk
index cdadcba..d864ad6 100644
--- a/src/webmcp/index.njk
+++ b/src/webmcp/index.njk
@@ -1,53 +1,64 @@
---
layout: layouts/base.njk
title: The Interdependency WebMCP
-description: Browser-native, read-only WebMCP access to the commit-pinned The Interdependency skill registry.
+description: Live browser-native WebMCP and remote MCP access to the commit-pinned The Interdependency skill registry.
permalink: /webmcp/
webmcp: true
---
WebMCP Challenge · implemented public surface WebMCP Challenge · live public surface This page is both the human-readable boundary and the browser-native WebMCP provider for The Interdependency skill registry. The website owns the registered tools; The page is the provider. Humans can operate the registry here; WebMCP-capable browsers receive the same five tools through Remote MCP: Loading the commit-pinned skill registry and checking this browser for WebMCP support… Loading the commit-pinned registry and checking browser WebMCP support… Checking remote MCP health… Registry source: resolving…The Interdependency WebMCP
- The-Interdependency/skill-lib remains the canonical source for skill definitions.document.modelContext.registerTool(...); remote MCP clients connect to the website-owned Render runtime.https://the-interdependency-mcp.onrender.com/mcpRuntime status
- LIVE
+ Registered v0 tools
-
-
-tiw_registry_statustiw_list_skillstiw_find_skilltiw_inspect_skilltiw_resolve_skill_closure
The controls below execute the same read-only registry logic exposed to agents.
-skill-lib/SKILL.md + skills.json - ↓ commit-pinned build projection -interdependentway.org skill registry - ↓ browser-native WebMCP registration -read-only agent tools-
The browser tools do not mutate GitHub, install skills, change canon, or create a competing skill definition. Their job is discovery, inspection, and dependency resolution.
++ + +
+ + + + + + + +Choose STATUS, LIST, FIND, INSPECT, or RESOLVE CLOSURE.
Open this route in a WebMCP-capable browser or in-app browser, then ask the browser agent to find or inspect an Interdependency skill. For example: “Find the skill for auditing and repairing a repository, then resolve its dependency closure.”
-In an ordinary browser without WebMCP support, this page remains a truthful status and documentation surface; it does not fake tool availability.
+skill-lib/SKILL.md + skills.json + ↓ commit-pinned projection +website registry logic + ├── browser WebMCP provider + └── remote Streamable HTTP MCP server+
The website owns both delivery surfaces. The-Interdependency/skill-lib remains authority for the skills themselves. All v0 operations are read-only.