From b31006af8290d125d3f9c76be6fa06d87c525109 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:05:59 -0700 Subject: [PATCH 01/14] Expose all verified skill-lib records to the WebMCP registry --- src/assets/js/webmcp-registry.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/assets/js/webmcp-registry.js b/src/assets/js/webmcp-registry.js index 4388df4..4093c0c 100644 --- a/src/assets/js/webmcp-registry.js +++ b/src/assets/js/webmcp-registry.js @@ -1,6 +1,6 @@ // === MODULE_BUILD === // id: webmcp_skill_registry_adapter -// purpose: Provide deterministic read-only operations over the public human-and-agent view of the website's commit-pinned skill-lib registry projection. +// purpose: Provide deterministic read-only operations over every registered skill in the website's commit-pinned skill-lib 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 === @@ -12,9 +12,9 @@ // 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 msdmd metadata-block applications, the exact meta skill, fresh-making, and epac-selection-display, matching the human card catalogue +// id: webmcp_full_catalogue_matches_source_registry +// given: the verified skill-lib projection contains both metadata-block and procedural skills +// then: public list/find/inspect/closure expose every registered skill and its source, matching the human catalogue // class: correctness // // id: webmcp_registry_smallest_dependency_closure @@ -28,13 +28,6 @@ function normalizeText(value) { return String(value || '').trim().toLowerCase(); } -function isPresentedSkill(skill) { - return skill?.kind === 'metadata-block' - || skill?.name === 'meta' - || skill?.name === 'fresh-making' - || skill?.name === 'epac-selection-display'; -} - export function humanTitle(description) { const text = String(description || '').trim(); if (!text) return ''; @@ -62,13 +55,13 @@ export function createSkillRegistry(registryData) { } const source = registryData.source; - const skills = registryData.skills.filter(isPresentedSkill); + const skills = registryData.skills; 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}`); + throw new Error(`registered skill dependency is not registered: ${skill.name} -> ${dependency}`); } } } @@ -142,7 +135,7 @@ export function createSkillRegistry(registryData) { registry_version: registryData.version, skill_count: skills.length, source_skill_count: registryData.skills.length, - public_scope: 'metadata-block plus meta plus fresh-making plus epac-selection-display', + public_scope: 'all registered skill-lib skills', source: { ...source }, fallback: Boolean(registryData.fallback), hmmm: Array.isArray(registryData.hmmm) ? [...registryData.hmmm] : [] From d4ba0640cc061ba86ba0d85ea1b660e6428d323b Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:06:50 -0700 Subject: [PATCH 02/14] Render complete static skill catalogue and live MCP invocation panel --- src/webmcp/index.njk | 105 +++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 65 deletions(-) diff --git a/src/webmcp/index.njk b/src/webmcp/index.njk index 10e069c..5a32c77 100644 --- a/src/webmcp/index.njk +++ b/src/webmcp/index.njk @@ -1,7 +1,7 @@ --- layout: layouts/base.njk title: The Interdependency WebMCP -description: Live browser-native WebMCP over shared skill and repository material. +description: Full skill-lib catalogue, browser-native handoff, and explicit MCP tool invocation. permalink: /webmcp/ webmcp: true --- @@ -50,85 +50,37 @@ webmcp: true

Shared human + agent skill material

2. Choose how the agent should work

Choose a repository first to enable its agent-work choices.

-

The public skill surface is deliberately narrow: the metadata skills built on the module self-declared metadata convention, the METAPAT consultation router, deterministic refresh of derived artifacts, and receipt-backed EPAC selection/display. The remaining skills stay canonical in the source library rather than becoming competing definitions here.

+

All {{ generated.skillRegistry.skills.length }} skills in the verified skill-lib registry are listed below. This is a commit-pinned readout, not a second definition. A skill describes how an authorized agent should work; being listed does not prove an executor is connected.

- - Presented skills shown + + {{ generated.skillRegistry.skills.length }} registered skills
-

Metadata skills

-
+ {% for group in [{ "kind": "metadata-block", "heading": "Metadata skills" }, { "kind": "procedural", "heading": "Procedural skills" }] %} +

{{ group.heading }}

+
{% for skill in generated.skillRegistry.skills %} - {% if skill.kind == "metadata-block" %} + {% if skill.kind == group.kind %}
-

metadata skill

+

{{ group.kind }}

{{ skill.human_title or skill.name }}

+

{{ skill.name }}

Description

{{ skill.description }}

+ {% if skill.depends_on and skill.depends_on.length %} +

Required skills: {{ skill.depends_on | join(", ") }}

+ {% endif %}

-

Read the canonical skill definition

-
- {% endif %} - {% endfor %} -
- -

Consultation router

-
- {% for skill in generated.skillRegistry.skills %} - {% if skill.name == "meta" %} - - {% endif %} - {% endfor %} -
- -

refresh

-
- {% for skill in generated.skillRegistry.skills %} - {% if skill.name == "fresh-making" %} - - {% endif %} - {% endfor %} -
- -

EPAC display

-
- {% for skill in generated.skillRegistry.skills %} - {% if skill.name == "epac-selection-display" %} - {% endif %} {% endfor %}
+ {% endfor %}
Selected skill @@ -151,6 +103,29 @@ webmcp: true
Select a repository first, then a skill. No internal repository or skill identifier needs to be typed.
+
+

Explicit tool invocation · live MCP response

+

4. Connect and call the MCP server

+

The public Interdependency MCP server provides read-only registry tools. To enact a skill, connect an authorized Streamable HTTP MCP executor that actually advertises tiw_execute_skill. The website never invents an execution result or supplies execution authority.

+

Only use an MCP endpoint you trust. Browser requests go directly to that endpoint; it must allow your browser origin and return JSON MCP responses. A private VM service requires its own approved gateway or client connection.

+ + + + +

+

No MCP tools discovered. The public server can be queried without a credential.

+ + + + +

+ + +

+

Skill execution requires a live executor that advertises tiw_execute_skill, a selected repository and skill, and a stated outcome.

+
Live MCP results and explicit failures appear here.
+
+

Same material, meaningful human selection

current public repository projection + skill definitions
@@ -164,7 +139,7 @@ human chooses repository + reads the applicable skill
 agent receives the exact repository, skill set, and human request
                  ↓
 agent's separately authorized change tools
-

For the refresh skill, choosing it automatically supplies the standard make-fresh request; the human may edit it. For EPAC selection/display, the human names the target and representation; the server passes that request but does not construct or render the research artifact. The browser handoff itself remains read-only and page-session-only. Actual repository changes require the agent's separately authorized repository, filesystem, deployment, or other tools.

+

For fresh-making, choosing the skill supplies an editable standard request. For EPAC selection/display, the human supplies a target and presentation; a repository selection does not select canon. The browser-agent handoff remains read-only and page-session-only. A separate deliberate MCP execution request may invoke an authenticated tool on a connected executor, whose response and authorization remain the executor's responsibility.

-
hmmmRepository observation identifies the target; it does not authenticate the target, grant write authority, prove that every declared freshness executor is available, or prove that a selected EPAC source contains a committed renderer.
+
hmmmThe default public MCP endpoint has no skill executor. Discovering or selecting a skill cannot run it: execution is blocked until an independently authorized MCP service exposes the exact execution tool. Repository observations and skill documentation remain provenance, not credentials or proof of operational capability.
From 8fc32e3567458b11057eef6452e8948101c8c672 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:07:41 -0700 Subject: [PATCH 03/14] Add bounded browser MCP discovery and tool invocation client --- src/assets/js/mcp-client.js | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/assets/js/mcp-client.js diff --git a/src/assets/js/mcp-client.js b/src/assets/js/mcp-client.js new file mode 100644 index 0000000..9b26650 --- /dev/null +++ b/src/assets/js/mcp-client.js @@ -0,0 +1,105 @@ +// === MODULE_BUILD === +// id: browser_mcp_client +// purpose: Discover and invoke advertised Streamable HTTP MCP tools from an explicitly selected trusted endpoint. +// entrypoint: imported by /assets/js/webmcp.js +// tests: tests/mcp-client.test.mjs +// === END MODULE_BUILD === +// === BOUNDARIES === +// id: browser_mcp_client_boundary +// network: direct browser POST to the human-selected HTTPS MCP endpoint (or loopback development endpoint) +// storage: none; optional bearer credential stays in the DOM until page exit or endpoint change +// effects: tools/call can mutate; never call automatically and require explicit invocation from the UI +// response: only JSON MCP replies; SSE and session-required transports fail visibly +// === END BOUNDARIES === +// Usage: discoverMcpTools(url, { token }); invokeMcpTool(url, name, argumentsObject, { token }). +// Never treat registry skill descriptions as executable tool definitions. + +export const SKILL_EXECUTION_TOOL = 'tiw_execute_skill'; +export const MCP_PROTOCOL_VERSION = '2026-07-28'; +const MAX_RESPONSE_CHARS = 262144; +const MAX_ARGUMENT_CHARS = 65536; +let nextId = 1; + +export function normalizeMcpEndpoint(value) { + let url; + try { url = new URL(String(value || '').trim()); } + catch { throw new Error('MCP endpoint must be an absolute URL'); } + + const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); + if (!(url.protocol === 'https:' || (loopback && url.protocol === 'http:'))) { + throw new Error('MCP endpoint requires HTTPS (HTTP permitted only for loopback)'); + } + if (url.username || url.password || url.hash || url.search) { + throw new Error('MCP endpoint must not contain credentials, a query, or a fragment'); + } + return url.href; +} + +async function mcpRequest(endpoint, method, params, { token = '', fetchImpl = fetch, signal } = {}) { + if (method !== 'tools/list' && method !== 'tools/call') throw new Error('Unsupported MCP method'); + if (token && (typeof token !== 'string' || /[\r\n]/.test(token))) throw new Error('Invalid bearer token'); + const url = normalizeMcpEndpoint(endpoint); + const id = nextId++; + const headers = { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + 'mcp-protocol-version': MCP_PROTOCOL_VERSION, + 'mcp-method': method + }; + if (method === 'tools/call') headers['mcp-name'] = params.name; + if (token) headers.authorization = `Bearer ${token}`; + const body = JSON.stringify({ + jsonrpc: '2.0', id, method, + params: { ...params, _meta: { 'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION } } + }); + if (body.length > MAX_ARGUMENT_CHARS) throw new Error('MCP arguments exceed the request limit'); + + let response; + try { + response = await fetchImpl(url, { + method: 'POST', headers, body, + cache: 'no-store', credentials: 'omit', redirect: 'error', signal + }); + } catch { + throw new Error('MCP request failed: check endpoint availability, HTTPS, CORS, and network access'); + } + + if (!response.ok) throw new Error(`MCP HTTP ${response.status} ${response.statusText || ''}`.trim()); + const type = response.headers.get('content-type') || ''; + if (!/^application\/(?:json|[^;]+\+json)(?:\s*;|$)/i.test(type)) { + throw new Error('MCP endpoint did not return JSON (SSE/session transports require a separate adapter)'); + } + const size = Number(response.headers.get('content-length') || 0); + if (size > MAX_RESPONSE_CHARS) throw new Error('MCP response exceeds the display limit'); + const raw = await response.text(); + if (raw.length > MAX_RESPONSE_CHARS) throw new Error('MCP response exceeds the display limit'); + + let message; + try { message = JSON.parse(raw); } + catch { throw new Error('MCP endpoint returned malformed JSON'); } + if (message?.jsonrpc !== '2.0' || message.id !== id) throw new Error('MCP response identity mismatch'); + if (message.error) throw new Error(`MCP error ${message.error.code}: ${message.error.message || 'Unknown error'}`); + if (!message.result || typeof message.result !== 'object') throw new Error('MCP result missing'); + return message.result; +} + +export async function discoverMcpTools(endpoint, options = {}) { + const result = await mcpRequest(endpoint, 'tools/list', {}, options); + if (!Array.isArray(result.tools)) throw new Error('MCP tools/list returned no tool catalogue'); + const names = new Set(); + for (const tool of result.tools) { + if (!tool || typeof tool.name !== 'string' || !tool.name.trim() || names.has(tool.name)) { + throw new Error('MCP tools/list contains an invalid or duplicate tool name'); + } + names.add(tool.name); + } + return result.tools; +} + +export async function invokeMcpTool(endpoint, name, args = {}, options = {}) { + if (!name || typeof name !== 'string') throw new Error('A discovered MCP tool must be selected'); + if (!args || typeof args !== 'object' || Array.isArray(args)) { + throw new Error('MCP tool arguments must be a JSON object'); + } + return mcpRequest(endpoint, 'tools/call', { name, arguments: args }, options); +} From e5a1a26e686e77b7ad2ae06b30c8b19ef11974b3 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:09:28 -0700 Subject: [PATCH 04/14] Wire explicit remote MCP discovery and tool calls to full skills selection --- src/assets/js/webmcp.js | 204 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 189 insertions(+), 15 deletions(-) diff --git a/src/assets/js/webmcp.js b/src/assets/js/webmcp.js index 34169ad..5016f6b 100644 --- a/src/assets/js/webmcp.js +++ b/src/assets/js/webmcp.js @@ -1,17 +1,18 @@ import { createSkillRegistry } from './webmcp-registry.js'; +import { SKILL_EXECUTION_TOOL, discoverMcpTools, invokeMcpTool } from './mcp-client.js'; // === MODULE_BUILD === // id: interdependency_webmcp_surface -// purpose: Register website-owned read-only WebMCP skill tools and bind exact human-selected repository + skill + request into one ephemeral browser-agent handoff. +// purpose: Register read-only browser WebMCP tools, display every source skill and invoke independently authorized remote MCP tools on explicit human action. // entrypoint: /webmcp/ // tests: tests/webmcp.test.mjs; tests/webmcp-repository-output.test.mjs // === END MODULE_BUILD === // === BOUNDARIES === // id: interdependency_webmcp_surface_boundary -// network: same-origin GET of /assets/data/skill-registry.json plus read-only health GET to the website-owned Render MCP runtime -// storage: none +// network: same-origin registry GET, public MCP health GET, and human-initiated Streamable HTTP POST to an explicitly chosen MCP endpoint +// storage: none; user-entered MCP credentials remain page-memory only and are never logged or persisted // user_data: human-entered handoff text exists only in page memory and is returned only when the browser agent invokes the explicit handoff tool -// operational_effects: none; repository/skill selection, handoff publication, registry inspection, and dependency resolution do not mutate repositories or external systems +// operational_effects: browser-native handoff remains read-only; only explicit human-invoked external MCP tools may cause effects under their own authorization // authority: skill-lib owns skill definitions; the website build observes public repository identities; external changes require separately authorized agent tools // === END BOUNDARIES === // === CONTRACTS === @@ -29,8 +30,13 @@ import { createSkillRegistry } from './webmcp-registry.js'; // given: a human has selected a skill and repository and supplied or accepted an ordinary-language intent // then: no agent handoff exists until submit; submit registers one page-session read-only `tiw_human_handoff` carrying skill closure, registry provenance, repository identity, and human intent // class: human_in_loop +// id: webmcp_remote_tool_invocation_is_explicit_and_evidence_bearing +// given: a human connects to an MCP endpoint, discovers tools, and submits an explicit call +// then: only an advertised tool is invoked and its actual result or failure is displayed, without assuming skill execution +// class: authorization // === END CONTRACTS === -// Usage: on /webmcp/, choose a repository first, then choose a skill and describe the outcome. Changing the repository requires choosing the skill again; only explicit Send publishes the read-only page-session handoff. +// Usage: browse all registered skills; select repository, skill and outcome for browser-agent handoff. +// Separately connect a trusted MCP executor, discover its tools, and invoke explicitly. The public registry endpoint is read-only. const REGISTRY_URL = '/assets/data/skill-registry.json'; const REMOTE_MCP_BASE = 'https://the-interdependency-mcp.onrender.com'; @@ -198,7 +204,6 @@ function bindHumanCatalogue(registry) { const setSkillStageEnabled = repository => { const enabled = Boolean(repository); - if (filterInput) filterInput.disabled = !enabled; for (const skillButton of skillButtons) skillButton.disabled = !enabled; setText( skillStageStatusElement(), @@ -403,6 +408,174 @@ function bindHumanCatalogue(registry) { return { getSelectedName: () => selectedName, getSelectedRepository: () => selectedRepository(repositorySelect) }; } +function bindRemoteMcpInvocation(registry, selection) { + const endpointInput = document.querySelector('[data-mcp-endpoint]'); + const tokenInput = document.querySelector('[data-mcp-bearer]'); + const discoverButton = document.querySelector('[data-mcp-discover]'); + const toolSelect = document.querySelector('[data-mcp-tool]'); + const argsInput = document.querySelector('[data-mcp-arguments]'); + const callButton = document.querySelector('[data-mcp-call]'); + const enactButton = document.querySelector('[data-mcp-enact]'); + const toolsStatus = document.querySelector('[data-mcp-tool-status]'); + const executionStatus = document.querySelector('[data-mcp-execution-status]'); + const resultOutput = document.querySelector('[data-mcp-tool-result]'); + const intentInput = document.querySelector('[data-human-handoff-intent]'); + let discovered = []; + let boundEndpoint = ''; + let revision = 0; + let busy = false; + + const selectedTool = () => discovered.find(tool => tool.name === toolSelect?.value); + const executor = () => discovered.find(tool => tool.name === SKILL_EXECUTION_TOOL); + const selectedContext = () => { + const repository = selection.getSelectedRepository(); + const name = selection.getSelectedName(); + const humanRequest = String(intentInput?.value || '').trim(); + return repository?.observed_head_sha && name && humanRequest ? { repository, name, humanRequest } : null; + }; + const canEnact = () => Boolean(executor() && selectedContext() && !busy); + const updateButtons = () => { + if (callButton) callButton.disabled = busy || !selectedTool(); + if (enactButton) enactButton.disabled = !canEnact(); + if (discoverButton) discoverButton.disabled = busy; + }; + const setResult = value => setText(resultOutput, typeof value === 'string' ? value : jsonResult(value)); + const invalidateDiscovery = reason => { + revision += 1; + discovered = []; + boundEndpoint = ''; + if (toolSelect) { + toolSelect.replaceChildren(new Option('Discover tools first…', '')); + toolSelect.disabled = true; + } + setText(toolsStatus, reason, 'hmmm'); + setText(executionStatus, 'No connected skill executor is verified.', 'hmmm'); + setResult('No MCP invocation result for the current connection.'); + updateButtons(); + }; + const markContextChange = () => { + revision += 1; + setResult('Skill or repository context changed. Earlier MCP output is not evidence for this selection.'); + updateButtons(); + }; + endpointInput?.addEventListener('input', () => { + if (tokenInput) tokenInput.value = ''; + invalidateDiscovery('Endpoint changed. Rediscover tools; previous credentials cleared.'); + }); + tokenInput?.addEventListener('input', () => invalidateDiscovery('Credential changed. Rediscover tools.')); + document.querySelector('[data-human-repository-target]')?.addEventListener('change', markContextChange); + intentInput?.addEventListener('input', markContextChange); + for (const card of document.querySelectorAll('[data-human-skill]')) { + card.querySelector('[data-select-skill]')?.addEventListener('click', markContextChange); + } + + const execute = async (tool, args, { requiresConsent = false } = {}) => { + if (!tool || !discovered.includes(tool)) return; + if (requiresConsent || tool.annotations?.readOnlyHint !== true) { + const confirmed = globalThis.confirm( + `Invoke MCP tool "${tool.name}" on the selected endpoint? This tool may change external state.\n\nOnly the connected server enforces permissions.` + ); + if (!confirmed) return; + } + const requestRevision = revision; + busy = true; + updateButtons(); + setText(toolsStatus, `Calling ${tool.name}…`, 'hmmm'); + try { + const result = await invokeMcpTool(boundEndpoint, tool.name, args, { token: tokenInput?.value || '' }); + if (requestRevision !== revision) return; + setResult({ tool: tool.name, endpoint: boundEndpoint, result }); + setText(toolsStatus, result.isError ? `MCP tool ${tool.name} returned an error.` : `MCP tool ${tool.name} returned a response (not independent proof of a completed skill).`, result.isError ? 'hmmm' : 'implemented'); + if (tool.name === SKILL_EXECUTION_TOOL) { + setText(executionStatus, result.isError ? 'Executor reported failure; inspect its response.' : 'Executor replied. Verify its receipt and resulting state before marking work complete.', result.isError ? 'hmmm' : 'implemented'); + } + } catch (error) { + if (requestRevision !== revision) return; + setResult(`hmmm: ${error.message}`); + setText(toolsStatus, `MCP invocation blocked: ${error.message}`, 'hmmm'); + if (tool.name === SKILL_EXECUTION_TOOL) setText(executionStatus, 'Skill execution did not complete through this connection.', 'hmmm'); + } finally { + busy = false; + updateButtons(); + } + }; + + discoverButton?.addEventListener('click', async () => { + const requestRevision = ++revision; + busy = true; + discovered = []; + boundEndpoint = ''; + if (toolSelect) { + toolSelect.replaceChildren(new Option('Discover tools first…', '')); + toolSelect.disabled = true; + } + updateButtons(); + setText(toolsStatus, 'Discovering live MCP tools…', 'hmmm'); + try { + const endpoint = endpointInput?.value || ''; + const tools = await discoverMcpTools(endpoint, { token: tokenInput?.value || '' }); + if (requestRevision !== revision) return; + discovered = tools; + boundEndpoint = endpoint; + for (const tool of discovered) { + const effect = tool.annotations?.readOnlyHint === true ? 'read-only' : 'may change state'; + toolSelect?.add(new Option(`${tool.title || tool.name} · ${effect}`, tool.name)); + } + if (toolSelect) toolSelect.disabled = discovered.length === 0; + const readOnlyCount = discovered.filter(tool => tool.annotations?.readOnlyHint === true).length; + setText(toolsStatus, `Discovered ${discovered.length} MCP tools (${readOnlyCount} read-only). Only explicitly selected calls are sent.`, 'implemented'); + setText(executionStatus, executor() + ? 'Skill executor advertised. Select a repository, skill and outcome; execution will require explicit confirmation.' + : 'hmmm: This MCP endpoint advertises no tiw_execute_skill tool. It can be inspected, but cannot enact a selected skill.', executor() ? 'implemented' : 'hmmm'); + setResult({ discovered_tools: discovered.map(tool => ({ name: tool.name, description: tool.description, annotations: tool.annotations })) }); + } catch (error) { + if (requestRevision !== revision) return; + setText(toolsStatus, `MCP discovery blocked: ${error.message}`, 'hmmm'); + setText(executionStatus, 'No execution tool is available from this connection.', 'hmmm'); + setResult(`hmmm: ${error.message}`); + } finally { + busy = false; + updateButtons(); + } + }); + + toolSelect?.addEventListener('change', () => { + if (argsInput) argsInput.value = '{}'; + updateButtons(); + }); + callButton?.addEventListener('click', async () => { + const tool = selectedTool(); + if (!tool) return; + let args; + try { + args = JSON.parse(argsInput?.value || '{}'); + if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('Expected a JSON object'); + } catch (error) { + setResult(`hmmm: Invalid tool arguments: ${error.message}`); + return; + } + await execute(tool, args); + }); + enactButton?.addEventListener('click', async () => { + const context = selectedContext(); + const tool = executor(); + if (!context || !tool || busy) { + setText(executionStatus, 'Execution requires a registered tool, repository head, skill, and outcome.', 'hmmm'); + return; + } + const skill = registry.inspectSkill({ name: context.name }); + const envelope = { + target_repository: context.repository, + skill, + required_skills: registry.resolveSkillClosure({ name: context.name }), + skill_registry: registry.getRegistryStatus(), + human_request: context.humanRequest + }; + await execute(tool, envelope, { requiresConsent: true }); + }); + updateButtons(); +} + async function registerTool(tool) { const context = modelContext(); if (!context?.registerTool) throw new Error('WebMCP API unavailable during tool registration'); @@ -414,11 +587,12 @@ export async function registerInterdependencyWebMCP() { const registry = createSkillRegistry(data); const status = registry.getRegistryStatus(); updateSource(status); - bindHumanCatalogue(registry); + const selection = bindHumanCatalogue(registry); + bindRemoteMcpInvocation(registry, selection); void checkRemoteMcp(); if (!modelContext()?.registerTool) { - setText(statusElement(), `Registry live for ${status.skill_count} public skills. Browser WebMCP registration requires a WebMCP-capable browser; the human catalogue remains usable.`, 'hmmm'); + setText(statusElement(), `Registry live for ${status.skill_count} registered skills. Browser WebMCP registration requires a WebMCP-capable browser; the catalogue and direct MCP calls remain usable.`, 'hmmm'); return { registered: false, reason: 'webmcp-unavailable', registry: status }; } if (globalThis.__interdependencyWebMcpRegistered) return { registered: true, reused: true, registry: status }; @@ -434,8 +608,8 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_list_skills', - title: 'List public Interdependency skills', - description: 'List the same curated public skill set shown to the human: msdmd metadata-block applications, METAPAT meta, fresh-making, and evidence-bound EPAC selection/display.', + title: 'List registered Interdependency skills', + description: 'List every verified registered skill shown to the human, including procedural and metadata-block skills.', inputSchema: { type: 'object', properties: { kind: { type: 'string' } }, additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, execute: async input => jsonResult(registry.listSkills(input)) @@ -443,8 +617,8 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_find_skill', - title: 'Find a public Interdependency skill', - description: 'Search the same curated public skill material the human can browse by task words, skill name, path, and description.', + title: 'Find a registered Interdependency skill', + description: 'Search the same complete registered skill material the human can browse by task words, skill name, path, and description.', inputSchema: { type: 'object', properties: { @@ -461,7 +635,7 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_inspect_skill', - title: 'Inspect a public Interdependency skill', + title: 'Inspect a registered Interdependency skill', description: 'Return the same selected skill material shown to the human: kind, description, dependencies, canonical path, and commit-pinned source URL.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, @@ -470,7 +644,7 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_resolve_skill_closure', - title: 'Resolve public Interdependency skill closure', + title: 'Resolve registered Interdependency skill closure', description: 'Resolve the smallest dependency-first transitive public skill set required by one selected public skill.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, @@ -478,7 +652,7 @@ export async function registerInterdependencyWebMCP() { }); globalThis.__interdependencyWebMcpRegistered = true; - setText(statusElement(), `WebMCP LIVE · five read-only registry tools over ${status.skill_count} public skills. A sixth handoff tool appears only after you press Send.`, status.fallback ? 'hmmm' : 'implemented'); + setText(statusElement(), `WebMCP LIVE · five read-only registry tools over ${status.skill_count} registered skills. A sixth handoff tool appears only after Send; separate direct MCP calls are human-initiated.`, status.fallback ? 'hmmm' : 'implemented'); return { registered: true, tools: 5, dynamic_handoff_tool: HANDOFF_TOOL_NAME, registry: status }; } From 4b6e94d1266ac9ebaf6e28837235a390d1dffbd8 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:10:34 -0700 Subject: [PATCH 05/14] Update WebMCP tests for full skill catalogue and live tool client --- tests/webmcp.test.mjs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/tests/webmcp.test.mjs b/tests/webmcp.test.mjs index 131a498..b41ca2e 100644 --- a/tests/webmcp.test.mjs +++ b/tests/webmcp.test.mjs @@ -64,17 +64,17 @@ test('committed or refreshed fallback snapshot is exact, usable, and retains sou assert.ok(snapshot.skills.some(skill => skill.name === 'repo-audit-repair')); }); -test('public registry presents msdmd, meta, fresh-making, and EPAC display while hiding unrelated specialist skills', () => { +test('registry exposes all source skills with exact dependency closure', () => { const registry = createSkillRegistry(sampleProjection()); assert.deepEqual( registry.listSkills().map(skill => skill.name), - ['msdmd', 'cap-build', 'meta', 'fresh-making', 'epac-selection-display'] + ['msdmd', 'cap-build', 'meta', 'fresh-making', 'epac-selection-display', 'repo-audit-repair'] ); - assert.equal(registry.findSkills({ query: 'audit repository' }).length, 0); + assert.equal(registry.findSkills({ query: 'audit repository' })[0].name, 'repo-audit-repair'); assert.equal(registry.findSkills({ query: 'capability' })[0].name, 'cap-build'); assert.equal(registry.findSkills({ query: 'derived artifact' })[0].name, 'fresh-making'); assert.equal(registry.findSkills({ query: 'EPAC molecule' })[0].name, 'epac-selection-display'); - assert.throws(() => registry.inspectSkill({ name: 'repo-audit-repair' }), /unknown public skill/); + assert.equal(registry.inspectSkill({ name: 'repo-audit-repair' }).canonical_path, 'repo-audit-repair/SKILL.md'); assert.deepEqual(registry.resolveSkillClosure({ name: 'cap-build' }).map(skill => skill.name), ['msdmd', 'cap-build']); assert.deepEqual(registry.resolveSkillClosure({ name: 'fresh-making' }).map(skill => skill.name), ['fresh-making']); @@ -83,9 +83,9 @@ test('public registry presents msdmd, meta, fresh-making, and EPAC display while ['epac-selection-display'] ); const status = registry.getRegistryStatus(); - assert.equal(status.skill_count, 5); + assert.equal(status.skill_count, 6); assert.equal(status.source_skill_count, 6); - assert.equal(status.public_scope, 'metadata-block plus meta plus fresh-making plus epac-selection-display'); + assert.equal(status.public_scope, 'all registered skill-lib skills'); }); test('WebMCP provider registers five base registry tools and one explicit dynamic browser handoff', async () => { @@ -152,12 +152,12 @@ test('dedicated WebMCP route presents collapsible skill cards, every generated r assert.match(page, /permalink: \/webmcp\//); assert.match(page, /webmcp: true/); assert.match(page, /The page is the provider/); - assert.match(page, /skill\.kind == "metadata-block"/); - assert.match(page, /skill\.name == "meta"/); - assert.match(page, /skill\.name == "fresh-making"/); - assert.match(page, /skill\.name == "epac-selection-display"/); - assert.match(page, /data-skill-group="epac"/); - assert.match(page, /Select and display EPAC/); + assert.match(page, /All \{\{ generated\.skillRegistry\.skills\.length \}\} skills/); + assert.match(page, /skill\.kind == group\.kind/); + assert.match(page, /data-skill-group="\{\{ group\.kind \}\}"/); + assert.match(page, /"metadata-block"/); + assert.match(page, /"procedural"/); + assert.match(page, /generated\.skillRegistry\.skills\.length/); assert.match(page, /
/); assert.match(page, /Description<\/summary>/); assert.match(page, /generated\.repos\.repositories/); @@ -170,8 +170,12 @@ test('dedicated WebMCP route presents collapsible skill cards, every generated r assert.match(page, /Nothing is sent merely by selecting or typing/); assert.match(page, /Send repository \+ skill \+ request to agent/); assert.match(page, /No internal repository or skill identifier needs to be typed/); - assert.match(page, /data-human-skill-filter[^>]+disabled/); + assert.doesNotMatch(page, /data-human-skill-filter[^>]+disabled/); assert.match(page, /data-select-skill[^>]+disabled/); + assert.match(page, /data-mcp-endpoint/); + assert.match(page, /data-mcp-discover/); + assert.match(page, /data-mcp-tool-result/); + assert.match(page, /data-mcp-enact disabled/); const repositoryStep = page.indexOf('1. Choose the repository'); const skillStep = page.indexOf('2. Choose how the agent should work'); const outcomeStep = page.indexOf('3. State the outcome and send'); @@ -198,5 +202,8 @@ test('human-readable surface keeps machine identifiers out of the visible readin assert.doesNotMatch(source, /\.slice\(0, 12\)/); assert.doesNotMatch(source, /status\.source\.commit/); assert.match(source, /The Interdependency skill library · commit-pinned verified snapshot/); + assert.match(source, /bindRemoteMcpInvocation\(registry, selection\)/); + assert.match(source, /SKILL_EXECUTION_TOOL/); + assert.match(source, /if \(requiresConsent \|\| tool\.annotations\?\.readOnlyHint !== true\)/); assert.match(source, /skill\.human_title \|\| skill\.name/); }); From 595d681ad25986d806661f9e5b242d2b3d2a0e91 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:11:17 -0700 Subject: [PATCH 06/14] Test real MCP roundtrip, explicit credential boundary and failure modes --- tests/mcp-client.test.mjs | 120 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/mcp-client.test.mjs diff --git a/tests/mcp-client.test.mjs b/tests/mcp-client.test.mjs new file mode 100644 index 0000000..e564ddd --- /dev/null +++ b/tests/mcp-client.test.mjs @@ -0,0 +1,120 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { createInterdependencyMcpServer } from '../server/mcp-server.mjs'; +import { + MCP_PROTOCOL_VERSION, SKILL_EXECUTION_TOOL, + normalizeMcpEndpoint, discoverMcpTools, invokeMcpTool +} from '../src/assets/js/mcp-client.js'; + +// === CHECKS === +// id: browser_mcp_real_roundtrip +// proves: webmcp_remote_tool_invocation_is_explicit_and_evidence_bearing +// call: self::mcp_roundtrip +// mutates: loopback_listener_only +// cleanup: close_listener +// +// id: browser_mcp_does_not_assume_execution +// proves: webmcp_remote_tool_invocation_is_explicit_and_evidence_bearing +// call: self::executor_discovery_and_result +// mutates: none +// === END CHECKS === + +const fixture = { + version: 1, + source: { repository: 'The-Interdependency/skill-lib', commit: '0123456789abcdef0123456789abcdef01234567', path: 'skills.json', sha256: 'fixture' }, + fallback: false, hmmm: [], + skills: [ + { name: 'msdmd', path: 'msdmd/SKILL.md', kind: 'metadata-block', depends_on: [], description: 'Metadata conventions.' }, + { name: 'repo-audit-repair', path: 'repo-audit-repair/SKILL.md', kind: 'procedural', depends_on: [], description: 'Audit and repair.' } + ] +}; + +test('endpoint validation requires HTTPS or loopback and forbids URL credentials', () => { + assert.equal(normalizeMcpEndpoint('https://example.org/mcp'), 'https://example.org/mcp'); + assert.equal(normalizeMcpEndpoint('http://127.0.0.1:3000/mcp'), 'http://127.0.0.1:3000/mcp'); + for (const url of ['http://example.org/mcp', 'https://user:secret@example.org/mcp', + 'https://example.org/mcp?token=secret', 'javascript:alert(1)', 'file:///etc/passwd']) { + assert.throws(() => normalizeMcpEndpoint(url)); + } +}); + +test('live MCP client calls the existing server and reads both procedural and metadata skills', async () => { + const server = createInterdependencyMcpServer(fixture); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const endpoint = `http://127.0.0.1:${server.address().port}/mcp`; + try { + const tools = await discoverMcpTools(endpoint); + assert.equal(tools.length, 5); + assert.ok(tools.every(tool => tool.annotations.readOnlyHint === true)); + assert.ok(!tools.some(tool => tool.name === SKILL_EXECUTION_TOOL)); + const result = await invokeMcpTool(endpoint, 'tiw_list_skills', {}); + assert.equal(result.isError, false); + assert.deepEqual(result.structuredContent.map(skill => skill.name), ['msdmd', 'repo-audit-repair']); + } finally { + server.close(); + await once(server, 'close'); + } +}); + +test('tool invocation uses advertised identity, a direct request, and no ambient browser credentials', async () => { + const seen = []; + const fakeFetch = async (url, init) => { + seen.push({ url, init }); + const body = JSON.parse(init.body); + return new Response(JSON.stringify({ + jsonrpc: '2.0', id: body.id, + result: { isError: false, structuredContent: { accepted: true }, content: [] } + }), { headers: { 'content-type': 'application/json' } }); + }; + const result = await invokeMcpTool('https://trusted.example/mcp', 'tiw_execute_skill', + { human_request: 'run the selected skill' }, + { token: 'test-secret', fetchImpl: fakeFetch }); + assert.equal(result.structuredContent.accepted, true); + assert.equal(seen.length, 1); + assert.equal(seen[0].url, 'https://trusted.example/mcp'); + assert.equal(seen[0].init.headers.authorization, 'Bearer test-secret'); + assert.equal(seen[0].init.headers['mcp-protocol-version'], MCP_PROTOCOL_VERSION); + assert.equal(seen[0].init.headers['mcp-method'], 'tools/call'); + assert.equal(seen[0].init.headers['mcp-name'], 'tiw_execute_skill'); + assert.equal(seen[0].init.credentials, 'omit'); + assert.equal(seen[0].init.redirect, 'error'); + assert.equal(JSON.parse(seen[0].init.body).params.arguments.human_request, 'run the selected skill'); +}); + +test('falsified tool response, SSE, bad identity and duplicate tools fail explicitly', async () => { + const response = (body, type = 'application/json') => async (_url, init) => new Response( + typeof body === 'function' ? JSON.stringify(body(JSON.parse(init.body))) : body, + { headers: { 'content-type': type } } + ); + await assert.rejects( + discoverMcpTools('https://trusted.example/mcp', { + fetchImpl: response(message => ({ jsonrpc: '2.0', id: message.id, result: { tools: [{ name: 'x' }, { name: 'x' }] } })) + }), /duplicate/ + ); + await assert.rejects( + discoverMcpTools('https://trusted.example/mcp', { + fetchImpl: response('data: stream', 'text/event-stream') + }), /did not return JSON/ + ); + await assert.rejects( + invokeMcpTool('https://trusted.example/mcp', 'test', {}, { + fetchImpl: response(() => ({ jsonrpc: '2.0', id: 'wrong', result: {} })) + }), /identity mismatch/ + ); + await assert.rejects( + invokeMcpTool('https://trusted.example/mcp', 'test', [], { fetchImpl: () => { throw new Error('should not be called'); } }), + /JSON object/ + ); +}); + +test('remote tool errors remain errors, not proof of an enacted skill', async () => { + const fakeFetch = async (_url, init) => new Response(JSON.stringify({ + jsonrpc: '2.0', id: JSON.parse(init.body).id, + result: { isError: true, content: [{ type: 'text', text: 'runner unavailable' }] } + }), { headers: { 'content-type': 'application/json' } }); + const result = await invokeMcpTool('https://trusted.example/mcp', 'tiw_execute_skill', {}, { fetchImpl: fakeFetch }); + assert.equal(result.isError, true); + assert.match(result.content[0].text, /runner unavailable/); +}); From b16a41ae2bce803dee1738f34e40a0fbf96c011c Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:11:48 -0700 Subject: [PATCH 07/14] Run MCP client protocol tests in normal site gates --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9e7eb96..73cc335 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "validate": "node scripts/validate-content.mjs && node scripts/verify-generated-routes.mjs && node scripts/verify-article-canon.mjs", "build": "npm run validate && eleventy && pagefind --site _site && node scripts/write-build-info.mjs", "pretest": "node scripts/prepare-tests.mjs", - "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/org-msdmd.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/project-docs.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sdk22-contract.test.mjs tests/sitrep.test.mjs tests/webmcp.test.mjs tests/webmcp-repository-output.test.mjs tests/mcp-server.test.mjs tests/builder-history.test.mjs tests/forge.test.mjs tests/support-links.test.mjs tests/support-only-deployment.test.mjs", - "test:mcp": "node --test tests/mcp-server.test.mjs", + "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/org-msdmd.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/project-docs.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sdk22-contract.test.mjs tests/sitrep.test.mjs tests/webmcp.test.mjs tests/mcp-client.test.mjs tests/webmcp-repository-output.test.mjs tests/mcp-server.test.mjs tests/builder-history.test.mjs tests/forge.test.mjs tests/support-links.test.mjs tests/support-only-deployment.test.mjs", + "test:mcp": "node --test tests/mcp-server.test.mjs tests/mcp-client.test.mjs", "start:mcp": "node server/mcp-server.mjs", "test:generated": "node --test tests/generated-site.test.mjs tests/human-ui-generated.test.mjs tests/textbook-generated.test.mjs tests/math-generated.test.mjs && node tests/links.test.mjs", "test:browser": "playwright test", From eb76b9a127498eb3a03728888b51b778f8553c9d Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:12:57 -0700 Subject: [PATCH 08/14] Document exact executor envelope, permissions, receipts and blocked state --- docs/webmcp-execution.md | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/webmcp-execution.md diff --git a/docs/webmcp-execution.md b/docs/webmcp-execution.md new file mode 100644 index 0000000..3bdd73d --- /dev/null +++ b/docs/webmcp-execution.md @@ -0,0 +1,60 @@ +# Skill readout and remote MCP invocation + +The source of truth is [`The-Interdependency/skill-lib/skills.json`](https://github.com/The-Interdependency/skill-lib/blob/main/skills.json). The website publishes its verified, commit-pinned registry projection. Every registered skill, regardless of kind, appears in the human catalogue and the five read-only registry tools. + +## What exists + +- **Readout**: description, kind, dependencies, source URL, and dependency-first closure for each registered skill. The browser and remote registry expose the same set. +- **Browser-agent handoff**: repo selection, skill and outcome, followed by explicit **Send**; this publishes a page-session *read-only* WebMCP tool. It does not execute work. +- **Remote MCP client**: direct browser POST to the entered Streamable HTTP endpoint; **Discover** lists the server's actual tools. **Call selected MCP tool** invokes the exact discovered tool with human-entered JSON arguments. Errors and results are shown verbatim, without implying a completed skill. +- **Skill execution**: enabled only if the connected endpoint advertises an operational tool named `tiw_execute_skill`. The human must select a repository, skill, outcome and explicitly confirm execution. The default public endpoint does not advertise an executor. + +A skill's `SKILL.md` is an instruction contract, *not* a callable method. An MCP registry tool is not a general agent runner. A successful `tools/call` proves that the endpoint replied, not that a repository changed or a task succeeded. + +## Executor contract + +A separately authorized MCP executor may expose: + +```text +Tool: tiw_execute_skill +Transport: Streamable HTTP MCP, JSON tool responses +Arguments: + target_repository: { + name, canonical_url, observed_head_sha, default_branch, + status, category, identity_source + } + skill: { + name, kind, description, depends_on, human_title, + canonical_path, canonical_url + } + required_skills: [skill record, ...] (dependency-first) + skill_registry: { + registry_version, skill_count, source_skill_count, + public_scope, source: { repository, commit, path, sha256 }, + fallback, hmmm + } + human_request: string +``` + +The executor must independently revalidate the registry source digest, skill identity, repository permissions, current repo head, tool capabilities and authorization. If the observed head is stale it must block or explicitly rebind with a new approval, not treat it as authority. The server must fetch the actual canonical skill and prerequisites; their summaries are not sufficient instructions. + +The operational endpoint must provide an execution result and an evidence-bearing receipt: terminal status (`completed`, `failed`, `blocked`, `running`, or `accepted`), source/ref identities, authorized effects, tested outcomes, errors, and `hmmm` for unresolved constraints. A queued or accepted run is not `completed`. Require explicit permission for writes and privileged actions; never allow a public tokenless endpoint to run arbitrary repository, VM or shell operations. + +If execution needs a long-running runner or callbacks, the runner owns that job lifecycle. The website cannot upgrade a pending result to completed. A stable URL to an authenticated receipt may be provided by the runner, but the website retains no credential or job state. + +## Transport and security + +The first-party read-only endpoint is currently `https://the-interdependency-mcp.onrender.com/mcp`. It is not an executor. Changing its tool list would require a separately reviewed server authority change. + +The browser client supports JSON responses to stateless Streamable HTTP `tools/list` and `tools/call` requests. SSE-only, session-required and stdio MCP services need an approved bridge. HTTPS is required except for loopback development. Browser CORS must allow `https://interdependentway.org` with the needed headers. The client does not follow redirects, send cookies, persist or log tokens, or treat discovered tool descriptions as permissions. Optional bearer credentials are sent **only to the endpoint explicitly entered** and cleared when the endpoint changes; avoid entering sensitive credentials into an untrusted endpoint. + +No URL or client-side authorization can grant VM or GitHub authority: the *connected executor* must enforce it. The website does not proxy tokens into its own read-only MCP service. + +## Local verification + +```sh +npm run test:mcp +npm run check +``` + +The MCP client integration test opens a loopback instance of the actual read-only server, lists tools, and calls `tiw_list_skills`. Other tests reject invalid endpoints, bad responses, SSE, duplicate tools, and mismatched call identities. Full end-to-end skill enactment remains **hmmm** until an authorized executor exists and supplies a verifiable run receipt. From f6bb3bd90f2c5560fe34bf0de61c8b226529feaf Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:13:20 -0700 Subject: [PATCH 09/14] Describe the complete skill catalogue and authorization boundary --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 410f18b..5c1f10b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This site displays exact, provenance-bearing chapter snapshots. It does not merg - `/about-me/` preserves the founder-authored **In Service to Love** narrative. - `/home/` is the newcomer-oriented Start Here route, organized by visitor intent rather than repository or content type. - `/about-the-site/` owns orientation, construction practices, and verification limits; `/start/` preserves its former address. +- `/webmcp/` publishes the complete commit-pinned skill-lib catalogue and a browser-native agent handoff. A separate, human-triggered MCP client can discover and call actual tools at a trusted endpoint; a skill runs only when an authorized executor advertises and implements `tiw_execute_skill`. The default public MCP server remains read-only. See [`docs/webmcp-execution.md`](docs/webmcp-execution.md). - `/by-the-builder/` renders the append-only, attributed builder journal. - `/research/` is the evidence index over admitted studies, bounded findings, and visible gaps; `/research/method/` is its subordinate admission/review method. - `/preamble/` remains available within the canonical reading system and primary navigation. @@ -52,6 +53,7 @@ This site displays exact, provenance-bearing chapter snapshots. It does not merg npm install npm run dev npm run check +npm run test:mcp ``` Refresh only the textbook source set: From ce6b1f9a59c9b25bfe7cafafa3ff139d9ccaac3b Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:13:35 -0700 Subject: [PATCH 10/14] Append required builder journal witness for WebMCP change --- src/_data/builder.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/_data/builder.json b/src/_data/builder.json index e6586fb..a15ab32 100644 --- a/src/_data/builder.json +++ b/src/_data/builder.json @@ -124,5 +124,12 @@ "time": "06:57:00+00:00", "model": "model identity unavailable", "body": "A recovery path should preserve what it knows and make what it cannot observe explicit. Missing documentation is not proof that a project has none. The same limit applies to attribution: this runtime does not expose its exact model identity. Erin authorized recording that limitation for this public-site release." + }, + { + "id": "2026-10-17-contract-is-not-an-executor", + "date": "2026-10-17", + "time": "15:13-07:00", + "model": "GPT-6", + "body": "An instruction may be exact without being executable. A page that lists skills has created visibility, not action; a page that can send an MCP request has created contact, not authority. Each new connection makes one more boundary visible. The evidence that matters is the executor's admitted capability, its actual response, and the state that response can prove. Anything in between belongs in hmmm." } ] From ac50b59cb7f00e1b4d4990473d87bb4a8df6924c Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Sat, 17 Oct 2026 15:16:23 -0700 Subject: [PATCH 11/14] Expose skill library and live MCP from Projects navigation --- src/_includes/layouts/base.njk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index 7b0abf2..4d0af18 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -29,7 +29,7 @@ {% set inWay = page.url == "/way/" or "/way/" in page.url or "/lab/" in page.url or "/articles/" in page.url or "/narratives/" in page.url or "/works/" in page.url %} {% set inTextbook = "/chapters/" in page.url %} {% set inResearch = "/research/" in page.url %} -{% set inProjects = "/projects/" in page.url or page.url == "/sitrep/" or "/artifacts/" in page.url %} +{% set inProjects = "/projects/" in page.url or page.url == "/sitrep/" or "/artifacts/" in page.url or page.url == "/webmcp/" %} {% set inAbout = page.url == "/about/" or page.url == "/about-me/" or page.url == "/about-the-site/" or page.url == "/by-the-builder/" or page.url == "/accessibility/" %} @@ -72,6 +72,7 @@ Repository SITREP Artifacts Organization map + Skill library & MCP {% elif inAbout %}