UNPKG

@bottobot/td-mcp

Version:

TouchDesigner MCP Server v2.8.0 - 21 MCP tools, 629 operators with clean parameter data, 69 Python API classes, 14 tutorials, 32 workflow patterns. Includes version system, experimental techniques KB, core tool enhancements, and experimental build support

259 lines (228 loc) 9.96 kB
// TD-MCP v2.10 - Search Operators Tool // Delegates search to operatorDataManager.performDirectSearch() - single source of truth. // v2.8: Added optional 'version' parameter for compatibility filtering. // v2.10: Added 'type' enum filter (exact/fuzzy/tag) and totalResults in response. import { z } from "zod"; import { filterByVersion, normalizeVersion } from "../wiki/utils/version-filter.js"; const VALID_CATEGORIES = ["CHOP", "DAT", "SOP", "TOP", "MAT", "COMP", "POP"]; // Tool schema export const schema = { title: "Search TouchDesigner Operators", description: "Search for operators using advanced contextual analysis and ranking. " + "Supports multi-term queries, category/subcategory filtering, parameter search, typo tolerance, " + "search type selection (exact/fuzzy/tag), and optional TouchDesigner version compatibility filtering.", inputSchema: { query: z.string().describe("Search query"), category: z.string().optional().describe("Filter by category (CHOP, DAT, SOP, TOP, MAT, COMP, POP)"), subcategory: z.string().optional().describe("Filter by subcategory (e.g., 'Audio', 'Filters', 'Generators')"), type: z.enum(["exact", "fuzzy", "tag"]).optional().describe( "Search matching strategy: " + "'exact' = only full-word matches in name/displayName; " + "'fuzzy' = typo-tolerant contextual search (default); " + "'tag' = match against operator tags and keywords only" ), parameter_search: z.boolean().optional().describe("Search within parameter names and descriptions"), show_details: z.boolean().optional().describe("Show detailed information for each result"), limit: z.number().optional().describe("Maximum results to display (default: 20, max: 50)"), version: z.string().optional().describe( "Filter results to operators compatible with a specific TouchDesigner version " + "(e.g. '2023', '2022', '2021', '2020', '2019', '099'). " + "Operators introduced after this version are excluded." ) } }; // Tool handler export async function handler({ query, category, subcategory, type: searchType = "fuzzy", parameter_search = false, show_details = false, limit = 20, version }, { operatorDataManager }) { if (!operatorDataManager) { return { content: [{ type: "text", text: "Operator data manager not available. Server may still be initializing." }] }; } // Validate category input if (category) { const normalised = category.trim().toUpperCase(); if (!VALID_CATEGORIES.includes(normalised)) { return { content: [{ type: "text", text: `Unknown category "${category}".\n\nValid operator families are: ${VALID_CATEGORIES.join(", ")}.\n\nExample: search_operators({ query: "${query}", category: "CHOP" })` }] }; } category = normalised; } const displayLimit = Math.min(Math.max(limit, 1), 50); try { // Delegate to the single authoritative search implementation in the manager const searchResult = operatorDataManager.performDirectSearch(query, { category, subcategory, parameter_search, limit: 0 // Fetch all matches; we slice for display below }); let allResults = searchResult.results || []; // Apply search type filtering on top of the base results if (searchType === "exact") { const queryLower = query.toLowerCase().trim(); allResults = allResults.filter(result => { const op = result.entry || result; const name = (op.name || "").toLowerCase(); const displayName = (op.displayName || "").toLowerCase(); return name === queryLower || displayName === queryLower || name.includes(queryLower) || displayName.includes(queryLower); }); } else if (searchType === "tag") { const queryLower = query.toLowerCase().trim(); allResults = allResults.filter(result => { const op = result.entry || result; const tags = (op.tags || []).map(t => t.toLowerCase()); const keywords = (op.keywords || []).map(k => k.toLowerCase()); return tags.some(t => t.includes(queryLower)) || keywords.some(k => k.includes(queryLower)); }); } // "fuzzy" (default) uses all results from performDirectSearch as-is // Version filtering (optional) const canonicalVersion = version ? normalizeVersion(version) : null; if (canonicalVersion) { allResults = await filterByVersion(allResults, canonicalVersion); } const totalMatches = allResults.length; // Build category facets from full result set const categoryFacets = {}; for (const result of allResults) { const cat = result.category; if (cat) { categoryFacets[cat] = (categoryFacets[cat] || 0) + 1; } } const displayResults = allResults.slice(0, displayLimit); // Format response let text = `# Search Results for "${query}"\n`; const filters = []; if (category) filters.push(`Category: ${category}`); if (subcategory) filters.push(`Subcategory: ${subcategory}`); if (searchType && searchType !== "fuzzy") filters.push(`Search type: ${searchType}`); if (parameter_search) filters.push('Parameter search enabled'); if (canonicalVersion) filters.push(`TD Version: ${canonicalVersion}`); if (filters.length > 0) { text += `**Filters:** ${filters.join(' | ')}\n`; } text += '\n'; if (totalMatches === 0) { text += `No operators found matching "${query}".\n\n`; text += `**Search Tips:**\n`; text += `- Try a broader search term\n`; text += `- Remove category/subcategory filters\n`; text += `- Enable parameter search (parameter_search: true)\n`; text += `- Check spelling; fuzzy matching tolerates 1-character typos\n`; text += `- Valid categories: ${VALID_CATEGORIES.join(', ')}\n`; } else { text += `Found **${totalMatches}** operator${totalMatches === 1 ? '' : 's'}`; if (displayResults.length < totalMatches) { text += ` (showing top ${displayResults.length})`; } text += `:\n\n`; for (let i = 0; i < displayResults.length; i++) { const result = displayResults[i]; const operator = result.entry || result; const rank = i + 1; text += `## ${rank}. ${operator.displayName || operator.name}\n`; text += `**Category:** ${operator.category}`; if (operator.subcategory) { text += ` | **Subcategory:** ${operator.subcategory}`; } const relevance = result.relevanceScore ?? result.score ?? 0; text += ` | **Relevance:** ${Math.round(relevance)}\n\n`; const desc = operator.description || operator.summary || 'No description available.'; const shortDesc = desc.length > 200 ? desc.substring(0, 200) + '...' : desc; text += `${shortDesc}\n\n`; if (show_details) { // Parameter count const params = operator.parameters; if (params && params.length > 0) { text += `**Parameters:** ${params.length} total\n`; if (parameter_search && result.matches) { const paramMatches = result.matches.filter(m => m.field === 'parameter'); if (paramMatches.length > 0) { text += `Matching params: ${paramMatches.map(m => m.content).join(', ')}\n`; } } text += '\n'; } // Keywords if (operator.keywords && operator.keywords.length > 0) { const kw = operator.keywords.slice(0, 5); text += `**Keywords:** ${kw.join(', ')}`; if (operator.keywords.length > 5) { text += ` and ${operator.keywords.length - 5} more`; } text += '\n\n'; } // Tips matching query if (operator.tips && operator.tips.length > 0) { const queryLower = query.toLowerCase(); const matchingTips = operator.tips .filter(tip => tip.toLowerCase().includes(queryLower)) .slice(0, 2); if (matchingTips.length > 0) { text += `**Related Tips:**\n`; matchingTips.forEach(tip => { text += `- ${tip.substring(0, 150)}${tip.length > 150 ? '...' : ''}\n`; }); text += '\n'; } } // Match field summary if (result.matches && result.matches.length > 0) { const uniqueFields = [...new Set(result.matches.map(m => m.field))]; text += `**Matches found in:** ${uniqueFields.join(', ')}\n`; } } text += '---\n\n'; } if (totalMatches > displayLimit) { text += `*...and ${totalMatches - displayLimit} more results. Use limit parameter or add filters to narrow results.*\n\n`; } // Search statistics text += `## Search Statistics\n`; text += `- Total matches (totalResults): **${totalMatches}**\n`; text += `- Displayed: ${displayResults.length}\n`; text += `- Search type: ${searchType}\n`; if (Object.keys(categoryFacets).length > 0) { text += `- Results by category: `; text += Object.entries(categoryFacets) .sort((a, b) => b[1] - a[1]) .map(([cat, count]) => `${cat} (${count})`) .join(', '); text += '\n'; } } return { content: [{ type: "text", text }], // Structured metadata for programmatic consumers _meta: { totalResults: totalMatches, displayed: displayResults.length, searchType, limit: displayLimit } }; } catch (error) { console.error('[Search Tool] Error:', error); return { content: [{ type: "text", text: `Search error: ${error.message || 'Unknown error occurred'}` }] }; } }