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

267 lines (233 loc) 12.2 kB
/** * Search Python API documentation tool * Search across TouchDesigner Python classes, methods, and members. * v2.8: Added optional 'version' parameter for version-aware filtering. */ import { z } from "zod"; import { normalizeVersion, getPythonCompatInfo, getVersionIndex, loadPythonApiCompat } from "../wiki/utils/version-filter.js"; export const schema = { title: "Search Python API", description: "Search across TouchDesigner Python classes, methods, and members. " + "Optionally filter by TouchDesigner version to see only API available in that release.", inputSchema: { query: z.string().describe("Search query for Python API"), search_in: z.string().optional().describe("Where to search: 'all', 'classes', 'methods', 'members'"), category: z.string().optional().describe("Filter by category (e.g., 'Operator', 'Component', 'General')"), limit: z.number().optional().describe("Maximum number of results to return"), version: z.string().optional().describe( "Filter results to Python API available in a specific TD version " + "(e.g. '2023', '2022', '2021', '2020', '2019', '099'). " + "Methods/members introduced after this version are excluded." ) } }; export async function handler({ query, search_in = "all", category, limit = 20, version }, { operatorDataManager }) { console.log(`[search_python_api] Searching for: ${query}, search_in: ${search_in}, category: ${category}`); try { // Check if operatorDataManager is available if (!operatorDataManager) { console.error('[search_python_api] operatorDataManager is not available'); return { content: [{ type: "text", text: "Wiki system not initialized. The wiki system is not available for Python API queries." }] }; } const results = { classes: [], methods: [], members: [], total_results: 0 }; const queryLower = query.toLowerCase(); const pythonClasses = operatorDataManager.getPythonClasses(); console.log(`[search_python_api] Searching through ${pythonClasses.length} Python classes`); // Search through Python classes for (const classEntry of pythonClasses) { // Filter by category if specified if (category && classEntry.category !== category) { continue; } // Search in class names and descriptions if (search_in === "all" || search_in === "classes") { if (classEntry.className.toLowerCase().includes(queryLower) || (classEntry.description && classEntry.description.toLowerCase().includes(queryLower))) { results.classes.push({ class_name: classEntry.className, description: classEntry.description, category: classEntry.category, relevance: calculateRelevance(classEntry.className, query) }); } } // Search in methods if ((search_in === "all" || search_in === "methods") && classEntry.methods) { for (const method of classEntry.methods) { if (method.name.toLowerCase().includes(queryLower) || (method.description && method.description.toLowerCase().includes(queryLower)) || (method.signature && method.signature.toLowerCase().includes(queryLower))) { results.methods.push({ class_name: classEntry.className, method_name: method.name, signature: method.signature, description: method.description, relevance: calculateRelevance(method.name, query) }); } } } // Search in members if ((search_in === "all" || search_in === "members") && classEntry.members) { for (const member of classEntry.members) { if (member.name.toLowerCase().includes(queryLower) || (member.description && member.description.toLowerCase().includes(queryLower))) { results.members.push({ class_name: classEntry.className, member_name: member.name, type: member.returnType, read_only: member.readOnly, description: member.description, relevance: calculateRelevance(member.name, query) }); } } } } console.log(`[search_python_api] Found ${results.classes.length} classes, ${results.methods.length} methods, ${results.members.length} members`); // Version filtering (optional) const canonicalVersion = version ? normalizeVersion(version) : null; if (canonicalVersion) { const targetIdx = await getVersionIndex(canonicalVersion); const pyApiData = await loadPythonApiCompat(); if (targetIdx !== -1) { // Filter methods: keep only those whose addedIn <= target version results.methods = results.methods.filter(m => { const classData = pyApiData.classes[m.class_name]; if (!classData || !classData.methods) return true; const methodData = classData.methods[m.method_name]; if (!methodData || !methodData.addedIn) return true; const addedIdx = pyApiData.classes[m.class_name] ? getVersionIndex(methodData.addedIn) : Promise.resolve(0); // Sync fallback: compare strings directly against known order const versionOrder = ['099', '2019', '2020', '2021', '2022', '2023', '2024']; const addedIdxSync = versionOrder.indexOf(methodData.addedIn); return addedIdxSync === -1 || addedIdxSync <= targetIdx; }); // Filter members similarly results.members = results.members.filter(m => { const classData = pyApiData.classes[m.class_name]; if (!classData || !classData.members) return true; const memberData = classData.members[m.member_name]; if (!memberData || !memberData.addedIn) return true; const versionOrder = ['099', '2019', '2020', '2021', '2022', '2023', '2024']; const addedIdxSync = versionOrder.indexOf(memberData.addedIn); return addedIdxSync === -1 || addedIdxSync <= targetIdx; }); // Filter classes results.classes = results.classes.filter(cls => { const classData = pyApiData.classes[cls.class_name]; if (!classData || !classData.addedIn) return true; const versionOrder = ['099', '2019', '2020', '2021', '2022', '2023', '2024']; const addedIdxSync = versionOrder.indexOf(classData.addedIn); return addedIdxSync === -1 || addedIdxSync <= targetIdx; }); } } // Sort by relevance and limit results results.classes.sort((a, b) => b.relevance - a.relevance); results.methods.sort((a, b) => b.relevance - a.relevance); results.members.sort((a, b) => b.relevance - a.relevance); results.classes = results.classes.slice(0, limit); results.methods = results.methods.slice(0, limit); results.members = results.members.slice(0, limit); results.total_results = results.classes.length + results.methods.length + results.members.length; // Build formatted response text let text = `# Python API Search Results for "${query}"\n\n`; const filters = []; if (search_in !== "all") filters.push(`Search scope: ${search_in}`); if (category) filters.push(`Category: ${category}`); if (canonicalVersion) filters.push(`TD Version: ${canonicalVersion}`); if (filters.length > 0) { text += `**Filters:** ${filters.join(' | ')}\n\n`; } if (results.total_results === 0) { text += `No Python API results found for "${query}".\n\n`; text += `**Search Tips:**\n`; text += `• Try a broader search term\n`; text += `• Remove category filters\n`; text += `• Search across all sections (classes, methods, members)\n`; text += `• Check spelling\n`; } else { text += `Found **${results.total_results}** total results:\n\n`; // Show class results if (results.classes.length > 0) { text += `## Classes (${results.classes.length})\n\n`; results.classes.forEach((cls, i) => { text += `${i + 1}. **${cls.class_name}** (${cls.category})\n`; text += ` ${cls.description || 'No description available'}\n\n`; }); } // Show method results if (results.methods.length > 0) { text += `## Methods (${results.methods.length})\n\n`; results.methods.forEach((method, i) => { text += `${i + 1}. **${method.class_name}.${method.method_name}()**\n`; if (method.signature) { text += ` **Signature:** \`${method.signature}\`\n`; } if (method.description) { text += ` ${method.description}\n`; } text += `\n`; }); } // Show member results if (results.members.length > 0) { text += `## Members (${results.members.length})\n\n`; results.members.forEach((member, i) => { text += `${i + 1}. **${member.class_name}.${member.member_name}** (${member.type || 'Unknown'})`; if (member.read_only) { text += ` *[Read Only]*`; } text += `\n`; if (member.description) { text += ` ${member.description}\n`; } text += `\n`; }); } } // Summary text += `---\n`; text += `*TouchDesigner Python API search | ${results.total_results} results found*\n`; console.log(`[search_python_api] Returning ${results.total_results} total results`); return { content: [{ type: "text", text }] }; } catch (error) { console.error('[search_python_api] Error:', error); return { content: [{ type: "text", text: `Failed to search Python API documentation: ${error.message}` }] }; } } function calculateRelevance(text, query) { const textLower = text.toLowerCase(); const queryLower = query.toLowerCase(); // Exact match gets highest score if (textLower === queryLower) return 100; // Starts with query gets high score if (textLower.startsWith(queryLower)) return 80; // Contains as whole word if (textLower.includes(' ' + queryLower) || textLower.includes(queryLower + ' ')) return 60; // Contains query if (textLower.includes(queryLower)) return 40; return 0; }