From b94ebb0962a17474a9a7dd8a79822cf67261ead2 Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 11:43:24 +0800 Subject: [PATCH] WebMCP: make amerc agent-actionable as callable tools (0.81.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit amerc is the agent mercenary tavern — so expose it to AI agents browsing the page via Web Model Context Protocol (navigator.modelContext), the W3C browser tool API. An agent on amerc.ai can now act on tools instead of scraping the DOM: - amerc_browse_agents (query/tag) — list the roster - amerc_platform_stats — live classes/online/sessions - amerc_get_agent (id) — full class details + instances - amerc_open (section) — navigate the tavern - each has a valid JSON-Schema inputSchema; read tools carry readOnlyHint - registered only on the main domain, feature-detected ('modelContext' in navigator) + try/catch, so it no-ops with zero risk where unsupported - verified (polyfilled modelContext): all 4 tools register with valid schemas and their execute() calls hit the live API correctly refs: W3C webmcp proposal, webfuse WebMCP cheat sheet --- index.html | 2 +- package.json | 2 +- src/Scene2D.jsx | 2 +- src/main.jsx | 2 ++ src/webmcp.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/webmcp.js diff --git a/index.html b/index.html index 83e9275..49a3566 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - + diff --git a/package.json b/package.json index 0778f7c..5d56804 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.80.0-agentready", + "version": "0.81.0-webmcp", "private": true, "type": "module", "scripts": { diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 0153415..935dae9 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -10,7 +10,7 @@ import { api } from './api.js'; const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); -const RELEASE = '0.80.0-agentready'; +const RELEASE = '0.81.0-webmcp'; const NAV = [ { id: 'home', label: 'Tavern' }, diff --git a/src/main.jsx b/src/main.jsx index c27eefe..c2f6637 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -19,6 +19,8 @@ if (App === Scene2DApp) { for (const n of ['inside', 'castlefloors', 'victoria', 'barrel', 'princess', 'soldier', 'soldier_altcolor']) { const im = new Image(); im.src = `/scene2d/lpc/${n}.png`; } + // Expose amerc to AI agents as WebMCP tools (no-op where unsupported). + import('./webmcp.js').then((m) => m.registerAmercTools()).catch(() => {}); } // Catch render errors so a single bad component can't blank the whole app. diff --git a/src/webmcp.js b/src/webmcp.js new file mode 100644 index 0000000..15625ec --- /dev/null +++ b/src/webmcp.js @@ -0,0 +1,54 @@ +// WebMCP — expose amerc as callable tools to AI agents browsing the page +// (Web Model Context Protocol, navigator.modelContext). amerc is for agents, +// so an agent on amerc.ai can browse/query/navigate the tavern as tools +// instead of scraping the DOM. No-ops gracefully where WebMCP isn't supported. +import { api } from './api.js'; + +export function registerAmercTools() { + if (typeof navigator === 'undefined' || !('modelContext' in navigator)) return; + const mc = navigator.modelContext; + if (!mc || typeof mc.registerTool !== 'function') return; + try { + mc.registerTool({ + name: 'amerc_browse_agents', + description: 'Browse the amerc roster of hireable AI agents. Optionally filter by a free-text query or a single tag (e.g. backend, web, model).', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'free-text search over name, description and tags' }, + tag: { type: 'string', description: 'restrict to one tag' }, + }, + }, + annotations: { readOnlyHint: true }, + execute: async (input = {}) => { + const d = await api(`/agents/classes${input.tag ? `?tag=${encodeURIComponent(input.tag)}` : ''}`); + let classes = d.classes || []; + if (input.query) { + const q = String(input.query).toLowerCase(); + classes = classes.filter((c) => `${c.name} ${(c.tags || []).join(' ')} ${c.description || ''}`.toLowerCase().includes(q)); + } + return { count: classes.length, agents: classes.map((c) => ({ id: c.id, name: c.name, kind: c.kind, tags: c.tags, description: c.description, owner: c.owner, online: c.onlineCount, instances: c.instanceCount })) }; + }, + }); + mc.registerTool({ + name: 'amerc_platform_stats', + description: 'Get live amerc platform stats: agent classes published, instances online now, and active sessions.', + inputSchema: { type: 'object', properties: {} }, + annotations: { readOnlyHint: true }, + execute: async () => api('/agents/stats'), + }); + mc.registerTool({ + name: 'amerc_get_agent', + description: 'Get full details of one amerc agent class by its numeric id, including its instances and their online status.', + inputSchema: { type: 'object', properties: { id: { type: 'integer', description: 'the agent class id' } }, required: ['id'] }, + annotations: { readOnlyHint: true }, + execute: async (input) => api(`/agents/classes/${Number(input.id)}`), + }); + mc.registerTool({ + name: 'amerc_open', + description: 'Open a section of the amerc tavern in the browser so the user can see it.', + inputSchema: { type: 'object', properties: { section: { type: 'string', enum: ['home', 'agents', 'mansion', 'booth', 'account', 'status', 'changelog', 'signin'], description: 'which section to open' } }, required: ['section'] }, + execute: async (input) => { window.location.hash = `#/${input.section}`; return { ok: true, opened: input.section }; }, + }); + } catch { /* WebMCP API shape differs — ignore, the site is unaffected */ } +}