clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / site / assets / webmcp.js
webmcp.js 6.0K
1// WebMCP tools for the Nostr Protocol Manual.2// Spec: https://github.com/webmachinelearning/webmcp (document.modelContext).3// No-op when the browser exposes no model context; zero UI impact.4(function () {5 'use strict';67 var mc = (typeof document !== 'undefined' && document.modelContext) ||8 (typeof navigator !== 'undefined' && navigator.modelContext) || null;9 if (!mc) return;1011 // Resolve site root from this script's URL so tools work from / and /nips/.12 var root = new URL('..', document.currentScript.src).href;1314 var indexPromise = null;15 function loadIndex() {16 if (!indexPromise) {17 indexPromise = fetch(root + 'assets/nip-index.json').then(function (r) {18 if (!r.ok) throw new Error('index fetch failed: ' + r.status);19 return r.json();20 });21 }22 return indexPromise;23 }2425 function ok(payload) {26 var text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 1);27 return { content: [{ type: 'text', text: text }] };28 }2930 function brief(e) {31 return { nip: e.nip, title: e.title, status: e.status, category: e.category, url: root + e.url };32 }3334 function findNip(index, id) {35 var want = String(id).toUpperCase().replace(/^NIP-?/, '').padStart(2, '0');36 return index.find(function (e) { return e.nip === want; });37 }3839 var tools = [40 {41 name: 'search_nips',42 description: 'Search the Nostr NIP dictionary (99 chapters) by free text. Matches NIP number, title, summary, category, and kind numbers. Returns matching NIPs with their page URLs.',43 inputSchema: {44 type: 'object',45 properties: { query: { type: 'string', description: 'Free-text query, e.g. "encryption", "kind 1059", "zap"' } },46 required: ['query']47 },48 execute: function (args) {49 return loadIndex().then(function (index) {50 var q = args.query.toLowerCase().trim();51 var kindMatch = q.match(/kind\s*:?\s*(\d{1,5})/);52 var hits = index.filter(function (e) {53 if (kindMatch && e.kinds.indexOf(parseInt(kindMatch[1], 10)) !== -1) return true;54 return (e.nip + ' ' + e.title + ' ' + (e.summary || '') + ' ' + (e.category || '') + ' ' + e.tags.join(' '))55 .toLowerCase().indexOf(q) !== -1;56 });57 return ok({ matches: hits.slice(0, 20).map(brief), total: hits.length });58 });59 }60 },61 {62 name: 'list_nips',63 description: 'List all NIPs in the manual, optionally filtered by status (active | deprecated | merged) or category (e.g. Core, Payments, Encryption, Media, Relay Ops).',64 inputSchema: {65 type: 'object',66 properties: {67 status: { type: 'string', enum: ['active', 'deprecated', 'merged'], description: 'Filter by lifecycle status' },68 category: { type: 'string', description: 'Filter by chapter category (substring match)' }69 }70 },71 execute: function (args) {72 return loadIndex().then(function (index) {73 var hits = index.filter(function (e) {74 if (args.status && e.status !== args.status) return false;75 if (args.category && (e.category || '').toLowerCase().indexOf(args.category.toLowerCase()) === -1) return false;76 return true;77 });78 return ok({ nips: hits.map(brief), total: hits.length });79 });80 }81 },82 {83 name: 'get_nip',84 description: 'Get the structured record for one NIP: title, status (with deprecation note), category, tags, kind numbers, section anchors, created/updated dates, and neighbor chapters.',85 inputSchema: {86 type: 'object',87 properties: { nip: { type: 'string', description: 'NIP number, hex allowed, e.g. "01", "5A", "EE", or "NIP-57"' } },88 required: ['nip']89 },90 execute: function (args) {91 return loadIndex().then(function (index) {92 var e = findNip(index, args.nip);93 if (!e) return ok({ error: 'Unknown NIP "' + args.nip + '". Use search_nips or list_nips.' });94 var full = {};95 for (var k in e) full[k] = e[k];96 full.url = root + e.url;97 return ok(full);98 });99 }100 },101 {102 name: 'open_nip',103 description: 'Navigate this browser tab to a NIP chapter page, optionally jumping to a section anchor (see get_nip for anchors).',104 inputSchema: {105 type: 'object',106 properties: {107 nip: { type: 'string', description: 'NIP number, e.g. "01", "5A", or "contents" for the table of contents' },108 section: { type: 'string', description: 'Optional section anchor, e.g. "overview", "examples", "history"' }109 },110 required: ['nip']111 },112 execute: function (args) {113 if (String(args.nip).toLowerCase() === 'contents') {114 window.location.href = root + 'index.html';115 return Promise.resolve(ok('Navigating to the table of contents.'));116 }117 return loadIndex().then(function (index) {118 var e = findNip(index, args.nip);119 if (!e) return ok({ error: 'Unknown NIP "' + args.nip + '".' });120 var url = root + e.url + (args.section ? '#' + args.section : '');121 window.location.href = url;122 return ok('Navigating to NIP-' + e.nip + ' (' + e.title + ') at ' + url);123 });124 }125 },126 {127 name: 'get_page_outline',128 description: 'Get the outline of the currently open manual page: its title and every section heading with its anchor. Works without navigation.',129 inputSchema: { type: 'object', properties: {} },130 execute: function () {131 var sections = Array.prototype.map.call(132 document.querySelectorAll('h2.bar[id]'),133 function (h) { return { anchor: h.id, title: h.textContent.replace(/^\s*\d+\s*/, '').trim() }; }134 );135 return Promise.resolve(ok({ title: document.title, url: window.location.href, sections: sections }));136 }137 }138 ];139140 if (typeof mc.registerTool === 'function') {141 tools.forEach(function (t) { mc.registerTool(t); });142 } else if (typeof mc.provideContext === 'function') {143 mc.provideContext({ tools: tools });144 }145})();