Strudel's function reference as the MCPs' api_reference serves it: the reference tab's functions (website/src/jev/reference.mjs), which the build writes to /jev/reference.json (website/src/pages/jev/reference.json.js) from jsdoc's doc.json, read like the song catalog (catalog.ts). Static per deploy, so kept for five minutes per isolate.
Matches a search lists at most, and the characters of a query.
25export async function reference(env: Env): Promise<Fn[]> { 26 if (kept && Date.now() < kept.until) return kept.fns; 27 const res = await siteFile(env, REFERENCE_PATH); 28 if (!res.ok) throw new Error(`the site's reference answered ${res.status}`); 29 const { functions } = (await res.json()) as { functions?: Fn[] }; 30 if (!Array.isArray(functions)) throw new Error("the site's reference is malformed"); 31 kept = { fns: functions, until: Date.now() + TTL_MS }; 32 return functions; 33} 34export const forgetReference = () => void (kept = null); 35 36const firstSentence = (s: string) => (/^(.+?[.!?])(\s|$)/s.exec(s)?.[1] ?? s).replace(/\s+/g, ' ').slice(0, 160); 37const names = (f: Fn) => [f.name, ...f.synonyms];
One function whole, as text.
40export function describeFn(f: Fn): string { 41 const lines = [`${f.name}(${f.params.map((p) => p.name).join(', ')})`]; 42 if (f.synonyms.length) lines.push(`synonyms: ${f.synonyms.join(', ')}`); 43 if (f.tags.length) lines.push(`tags: ${f.tags.join(', ')}`); 44 lines.push('', f.description); 45 if (f.params.length) { 46 lines.push('', 'parameters:'); 47 for (const p of f.params) lines.push(`- ${p.name}${p.type ? ` (${p.type})` : ''}${p.description ? `: ${p.description}` : ''}`); 48 } 49 if (f.examples.length) { 50 lines.push('', 'examples:'); 51 for (const e of f.examples) lines.push(e); 52 } 53 return lines.join('\n'); 54}
api_reference: one function by name, or the functions matching query:
any of its words in a name, synonym, tag or the description. Each word
found weighs 3 when it is a name, 2 inside a name, 1 in a tag or the
description; the heaviest come first, then those matching more words.
60export function answerReference(fns: Fn[], args: { query?: unknown; name?: unknown }): string { 61 if (typeof args.name === 'string' && args.name.trim()) { 62 const name = args.name.trim().slice(0, MAX_QUERY); 63 const f = fns.find((x) => x.name === name) ?? fns.find((x) => x.synonyms.includes(name)); 64 if (!f) { 65 const near = fns.filter((x) => names(x).some((n) => n.toLowerCase().includes(name.toLowerCase()))).slice(0, 10); 66 throw new Error(`no function ${name}${near.length ? `; did you mean ${near.map((x) => x.name).join(', ')}?` : '; search with query'}`); 67 } 68 return describeFn(f); 69 } 70 if (typeof args.query !== 'string' || !args.query.trim()) throw new Error('pass query (words to search) or name (one function)'); 71 const words = [...new Set(args.query.toLowerCase().slice(0, MAX_QUERY).split(/[\s,]+/).filter(Boolean))]; 72 const rank = (f: Fn) => { 73 const own = names(f).map((n) => n.toLowerCase()); 74 const all = `${own.join(' ')} ${f.tags.join(' ')} ${f.description}`.toLowerCase(); 75 let hits = 0; 76 let weight = 0; 77 for (const w of words) { 78 if (!all.includes(w)) continue; 79 hits += 1; 80 weight += own.includes(w) ? 3 : own.some((n) => n.includes(w)) ? 2 : 1; 81 } 82 return { hits, weight }; 83 }; 84 const found = fns 85 .map((f) => ({ f, ...rank(f) })) 86 .filter((x) => x.hits > 0) 87 .sort((a, b) => b.weight - a.weight || b.hits - a.hits || a.f.name.localeCompare(b.f.name)); 88 if (!found.length) return `nothing in the reference matches "${args.query.slice(0, MAX_QUERY)}"`; 89 const shown = found.slice(0, MAX_MATCHES).map(({ f }) => `${f.name}${f.synonyms.length ? ` (${f.synonyms.join(', ')})` : ''}: ${firstSentence(f.description)}`); 90 const more = found.length > MAX_MATCHES ? `\n… ${found.length - MAX_MATCHES} more; narrow the query` : ''; 91 const some = words.length > 1 && found.some((x) => x.hits < words.length) ? ', best first' : ''; 92 return `${found.length} of ${fns.length} functions match${some}; api_reference with name for one whole:\n${shown.join('\n')}${more}`; 93}