1// WebMCP â expose site tools to AI agents via navigator.modelContext.registerTool 2// Spec: https://webmachinelearning.github.io/webmcp/ 3// Scanner checks that the page calls navigator.modelContext.registerTool 4 5(function () { 6 const tools = [ 7 { 8 name: 'search_site', 9 description: 'Search the portfolio site for projects, games, and articles', 10 inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] }, 11 execute: async ({ query }) => { 12 const q = String(query || '').toLowerCase() 13 // client-side search: scan visible sections 14 const results = [] 15 document.querySelectorAll('h2, h3, p, a').forEach((el) => { 16 const t = (el.textContent || '').toLowerCase() 17 if (t.includes(q) && results.length < 5) results.push(t.slice(0, 120)) 18 }) 19 return { query, results: results.length ? results : ['No matches on this page. Try https://luongnv.com/sitemap.xml'] } 20 }, 21 }, 22 { 23 name: 'open_game', 24 description: 'Open a browser game by slug', 25 inputSchema: { 26 type: 'object', 27 properties: { slug: { type: 'string', description: 'Game slug e.g. magic-webcam, flappy-bird' } }, 28 required: ['slug'], 29 }, 30 execute: async ({ slug }) => { 31 const url = `https://luongnv.com/games/${slug}/` 32 window.open(url, '_blank') 33 return { opened: url } 34 }, 35 }, 36 { 37 name: 'get_contact', 38 description: 'Get contact and social links', 39 inputSchema: { type: 'object', properties: {} }, 40 execute: async () => ({ 41 website: 'https://luongnv.com', 42 github: 'https://github.com/luongnv89', 43 linkedin: 'https://linkedin.com/in/luongnv89', 44 email: 'via https://luongnv.com/#contact', 45 }), 46 }, 47 { 48 name: 'list_projects', 49 description: 'List open-source projects and products', 50 inputSchema: { type: 'object', properties: {} }, 51 execute: async () => { 52 const projects = Array.from(document.querySelectorAll('#oss h3')).map((h) => h.textContent?.trim()).filter(Boolean) 53 return { projects: projects.length ? projects : ['claude-howto', 'agent-skill-manager', 'sleek-ui'] } 54 }, 55 }, 56 ] 57 58 function register() { 59 const mc = navigator.modelContext 60 if (!mc || typeof mc.registerTool !== 'function') return 61 const controller = new AbortController() 62 for (const t of tools) { 63 try { 64 mc.registerTool(t, { signal: controller.signal }) 65 } catch (e) { 66 console.warn('[webmcp] register failed', t.name, e) 67 } 68 } 69 // Unregister on page hide 70 window.addEventListener('pagehide', () => controller.abort(), { once: true }) 71 } 72 73 if (document.readyState === 'loading') { 74 document.addEventListener('DOMContentLoaded', register) 75 } else { 76 register() 77 } 78 // Retry once after 1s for late-injecting agents 79 setTimeout(register, 1000) 80})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.