PageSourceSearch

https://yaqeen.me/webmcp.js

js yaqeen.me collected 2026-09-25 21:46:14 UTC 3,120 bytes, 141 lines download raw bytes

1(function () {
2	const modelContext = navigator.modelContext;
3	if (!modelContext || typeof modelContext.registerTool !== 'function') {
4		return;
5	}
6
7	const controller = new AbortController();
8	const sections = {
9		home: '/',
10		about: '/about',
11		projects: '/projects',
12		blog: '/blog',
13		api_docs: '/docs/api',
14		llms: '/llms.txt'
15	};
16
17	function absolute(pathname) {
18		return new URL(pathname, window.location.origin).toString();
19	}
20
21	function registerTool(definition) {
22		modelContext.registerTool({
23			...definition,
24			signal: controller.signal
25		});
26	}
27
28	registerTool({
29		name: 'list_site_sections',
30		description: 'List the main yaqeen.me sections and discovery resources.',
31		inputSchema: {
32			type: 'object',
33			properties: {},
34			additionalProperties: false
35		},
36		execute: async function () {
37			return {
38				content: [
39					{
40						type: 'text',
41						text: JSON.stringify(
42							Object.entries(sections).map(function ([name, pathname]) {
43								return { name: name, url: absolute(pathname) };
44							}),
45							null,
46							2
47						)
48					}
49				]
50			};
51		}
52	});
53
54	registerTool({
55		name: 'search_blog_posts',
56		description: 'Search yaqeen.me blog posts by title, slug, and description.',
57		inputSchema: {
58			type: 'object',
59			properties: {
60				query: { type: 'string', minLength: 1 },
61				limit: { type: 'integer', minimum: 1, maximum: 10, default: 5 }
62			},
63			required: ['query'],
64			additionalProperties: false
65		},
66		execute: async function (input) {
67			const query = String(input.query || '')
68				.trim()
69				.toLowerCase();
70			const limit = Math.min(Math.max(Number(input.limit) || 5, 1), 10);
71
72			const response = await fetch('/blog/api/posts?limit=100');
73			if (!response.ok) {
74				throw new Error('Unable to load blog posts.');
75			}
76
77			const data = await response.json();
78			const matches = (data.allPosts || [])
79				.filter(function (post) {
80					const haystack = [post.title, post.slug, post.description].join(' ').toLowerCase();
81					return haystack.includes(query);
82				})
83				.slice(0, limit)
84				.map(function (post) {
85					return {
86						title: post.title,
87						description: post.description,
88						url: post.canonicalUrl || absolute(post.href),
89						isExternal: Boolean(post.isExternal)
90					};
91				});
92
93			return {
94				content: [
95					{
96						type: 'text',
97						text: JSON.stringify(matches, null, 2)
98					}
99				]
100			};
101		}
102	});
103
104	registerTool({
105		name: 'get_current_page_context',
106		description: 'Return the current yaqeen.me page title, URL, and description.',
107		inputSchema: {
108			type: 'object',
109			properties: {},
110			additionalProperties: false
111		},
112		execute: async function () {
113			const descriptionNode = document.querySelector('meta[name="description"]');
114
115			return {
116				content: [
117					{
118						type: 'text',
119						text: JSON.stringify(
120							{
121								title: document.title,
122								url: window.location.href,
123								description: descriptionNode ? descriptionNode.getAttribute('content') : ''
124							},
125							null,
126							2
127						)
128					}
129				]
130			};
131		}
132	});
133
134	window.addEventListener(
135		'pagehide',
136		function () {
137			controller.abort();
138		},
139		{ once: true }
140	);
141})();

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.