PageSourceSearch

https://luongnv.com/webmcp.js

js luongnv.com collected 2026-10-01 23:55:15 UTC 2,982 bytes, 80 lines download raw bytes

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.