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