WebMCP: make amerc agent-actionable as callable tools (0.81.0)

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
This commit is contained in:
artheru 2026-06-10 11:43:24 +08:00
parent 7cf9293bab
commit b94ebb0962
5 changed files with 59 additions and 3 deletions

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.80.0-agentready" />
<meta name="version" content="0.81.0-webmcp" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{
"name": "amerc-site",
"version": "0.80.0-agentready",
"version": "0.81.0-webmcp",
"private": true,
"type": "module",
"scripts": {

View File

@ -10,7 +10,7 @@ import { api } from './api.js';
const Menu = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>);
const X = ({ size = 20 }) => (<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>);
const RELEASE = '0.80.0-agentready';
const RELEASE = '0.81.0-webmcp';
const NAV = [
{ id: 'home', label: 'Tavern' },

View File

@ -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.

54
src/webmcp.js Normal file
View File

@ -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 */ }
}