Files
mcp-wms-wiki/src/tools/glossary-tool.js
T
2026-05-20 09:41:27 +02:00

145 lines
3.7 KiB
JavaScript

/**
* Simple Levenshtein distance for fuzzy matching of glossary terms.
*/
function levenshtein(a, b) {
const m = a.length;
const n = b.length;
const dp = Array.from({ length: m + 1 }, (_, i) => [i]);
for (let j = 1; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] =
a[i - 1] === b[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}
export const glossaryToolDefinition = {
name: 'get_glossary_term',
description:
'Look up a WMS-specific term, abbreviation, or acronym in the EasyWMS glossary (e.g. "LPN", "PDL", "ASN").',
inputSchema: {
type: 'object',
properties: {
term: {
type: 'string',
description: 'Term or abbreviation to look up (case-insensitive)',
},
},
required: ['term'],
},
};
export function handleGetGlossaryTerm(args, { glossary }) {
const { term } = args;
if (!term || term.trim().length === 0) {
return { content: [{ type: 'text', text: 'Error: term parameter is required.' }] };
}
const query = term.trim().toLowerCase();
// 1. Exact match
if (glossary.has(query)) {
const entry = glossary.get(query);
return {
content: [
{
type: 'text',
text: `**${entry.term}**\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
},
],
};
}
// 2. Partial match — prefer entries whose term STARTS WITH the query
// (handles "pdl" → "pdl (picking dedicated location)")
const startsWithMatches = [];
const containsMatches = [];
for (const [key, entry] of glossary) {
if (key.startsWith(query + ' ') || key.startsWith(query + '(') || key === query) {
startsWithMatches.push(entry);
} else if (key.includes(query)) {
containsMatches.push(entry);
}
}
// Single strong match
if (startsWithMatches.length === 1) {
const entry = startsWithMatches[0];
return {
content: [
{
type: 'text',
text: `**${entry.term}**\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
},
],
};
}
const partialMatches = startsWithMatches.length > 0 ? startsWithMatches : containsMatches;
if (partialMatches.length === 1) {
const entry = partialMatches[0];
return {
content: [
{
type: 'text',
text: `**${entry.term}** *(partial match)*\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
},
],
};
}
if (partialMatches.length > 1) {
const list = partialMatches
.slice(0, 5)
.map(e => `- **${e.term}**: ${e.definition.slice(0, 100)}...`)
.join('\n');
return {
content: [
{
type: 'text',
text: `Multiple matches for "${term}":\n\n${list}\n\n*Refine your query for an exact match.*`,
},
],
};
}
// 3. Fuzzy match (Levenshtein)
const candidates = [];
for (const [key, entry] of glossary) {
const dist = levenshtein(query, key);
if (dist <= 3) candidates.push({ entry, dist });
}
candidates.sort((a, b) => a.dist - b.dist);
if (candidates.length > 0) {
const suggestions = candidates
.slice(0, 3)
.map(c => `- **${c.entry.term}**`)
.join('\n');
return {
content: [
{
type: 'text',
text: `Term "${term}" not found in glossary. Did you mean:\n\n${suggestions}\n\n*See [Glossary](glossary.md) for the full list.*`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Term "${term}" not found in the EasyWMS glossary. See [Glossary](glossary.md) for the full list.`,
},
],
};
}