UNPKG

@access-mcp/compute-resources

Version:

MCP server for ACCESS-CI Compute Resources API

803 lines (782 loc) 38 kB
import { BaseAccessServer, handleApiError, sanitizeGroupId, resolveResourceId, FIELDS_OF_SCIENCE, RESOURCE_TYPES, ACCESS_SYSTEMS, GPU_SELECTION_GUIDE, MEMORY_REQUIREMENTS, getFeatureNames, } from "@access-mcp/shared"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const { version } = require("../package.json"); export class ComputeResourcesServer extends BaseAccessServer { constructor() { super("access-mcp-compute-resources", version, "https://operations-api.access-ci.org"); } getTools() { return [ { name: "search_resources", description: "Search ACCESS-CI compute resources (list, filter, get details). Returns resource IDs for other services. Returns {total, items}.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Get specific resource (e.g., 'delta.ncsa.access-ci.org')", }, query: { type: "string", description: "Search names, descriptions, organizations", }, type: { type: "string", enum: ["compute", "storage", "cloud", "gpu", "cpu"], description: "Filter by resource type", }, has_gpu: { type: "boolean", description: "Filter by GPU: true = only GPU resources, false = only non-GPU resources. Omit to include all resources.", }, organization: { type: "string", description: "Filter by org (NCSA, PSC, Purdue, SDSC, TACC)", }, include_ids: { type: "boolean", description: "Include resource IDs for other services", default: true, }, }, }, }, { name: "get_resource_hardware", description: "Get hardware specs (CPU, GPU, memory, storage). Returns detailed specs.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Resource name (e.g., 'Anvil', 'Delta') or full ID (e.g., 'anvil.purdue.access-ci.org')", }, }, required: ["id"], }, }, ]; } getResources() { return [ { uri: "accessci://compute-resources", name: "ACCESS-CI Compute Resources", description: "Information about ACCESS-CI compute resources, hardware, and software", mimeType: "application/json", }, { uri: "accessci://compute-resources/capabilities-matrix", name: "Compute Resource Capabilities Matrix", description: "Comparison of different resource types and their ideal use cases", mimeType: "application/json", }, { uri: "accessci://compute-resources/gpu-guide", name: "GPU Resource Selection Guide", description: "Guidance for selecting appropriate GPU resources for different workloads", mimeType: "text/markdown", }, { uri: "accessci://compute-resources/resource-types", name: "Resource Type Taxonomy", description: "Classification of ACCESS-CI resource types and their characteristics", mimeType: "application/json", }, ]; } getPrompts() { return [ { name: "recommend_compute_resource", description: "Get personalized recommendations for ACCESS-CI compute resources based on research needs", arguments: [ { name: "research_area", description: "Your field of research (e.g., 'machine learning', 'molecular dynamics', 'climate modeling', 'genomics')", required: true, }, { name: "compute_needs", description: "Describe your computational requirements in natural language (e.g., 'GPU for training transformers', 'high memory for genome assembly', '500TB storage for climate data', 'parallel CPU for CFD simulations')", required: true, }, { name: "experience_level", description: "Optional: Your HPC experience level (e.g., 'beginner', 'intermediate', 'advanced'). Helps tailor recommendations to your familiarity with supercomputing.", required: false, }, { name: "allocation_size", description: "Optional: Approximate allocation size needed (e.g., 'small pilot project', 'medium research project', 'large-scale production', or specific credit amount like '100000')", required: false, }, ], }, ]; } async handleToolCall(request) { const { name, arguments: args } = request.params; const toolArgs = (args || {}); try { switch (name) { case "search_resources": return await this.searchResourcesRouter({ resource_id: toolArgs.id, query: toolArgs.query, resource_type: toolArgs.type, has_gpu: toolArgs.has_gpu, organization: toolArgs.organization, include_resource_ids: toolArgs.include_ids, }); case "get_resource_hardware": return await this.getResourceHardware(toolArgs.id); default: return this.errorResponse(`Unknown tool: ${name}`); } } catch (error) { return this.errorResponse(handleApiError(error)); } } /** * Router for consolidated search_resources tool * Routes to appropriate handler based on parameters */ async searchResourcesRouter(args) { // Get specific resource details by ID if (args.resource_id) { return await this.getComputeResource(args.resource_id); } // No parameters = list all resources if (!args.query && !args.resource_type && !args.has_gpu && !args.organization) { return await this.listComputeResources(); } // Search/filter resources return await this.searchResources(args); } async handleResourceRead(request) { const { uri } = request.params; if (uri === "accessci://compute-resources") { return this.createTextResource(uri, "ACCESS-CI Compute Resources API - Use the available tools to query compute resources, hardware specifications, and software availability."); } if (uri === "accessci://compute-resources/capabilities-matrix") { const matrix = { resource_types: RESOURCE_TYPES, comparison_matrix: { "Machine Learning / AI": { primary: "GPU", secondary: "High Memory", rationale: "GPUs provide massive parallelism for training neural networks. High memory systems useful for large datasets that don't fit in GPU memory.", }, "Molecular Dynamics": { primary: "GPU", secondary: "CPU", rationale: "Modern MD codes (GROMACS, AMBER) are GPU-accelerated. CPU clusters still useful for older codes or ensemble simulations.", }, "Genomics / Bioinformatics": { primary: "High Memory", secondary: "CPU", rationale: "Genome assembly requires large memory. High-throughput analysis benefits from many CPU cores.", }, "Climate / Weather Modeling": { primary: "CPU", secondary: "Storage", rationale: "Large-scale parallel codes optimized for CPU clusters. Massive data output requires substantial storage.", }, "CFD / Engineering": { primary: "CPU", secondary: "GPU", rationale: "Traditional CFD codes use CPU clusters. Some modern codes support GPU acceleration.", }, "Data Analytics": { primary: "CPU", secondary: "High Memory", rationale: "Parallel data processing on CPU clusters. Large datasets may require high-memory systems.", }, "Quantum Chemistry": { primary: "CPU", secondary: "High Memory", rationale: "Ab initio calculations are CPU-intensive. Some calculations require significant memory.", }, }, selection_guide: { step_1: "Identify your primary computational pattern (CPU-bound, GPU-accelerated, memory-intensive, data-intensive)", step_2: "Match to resource type using the comparison matrix above", step_3: "Use search_resources tool to find specific systems with those capabilities", step_4: "Review hardware specs with get_resource_hardware tool", step_5: "Check software availability with software-discovery service", }, }; return this.createJsonResource(uri, matrix); } if (uri === "accessci://compute-resources/gpu-guide") { const guide = `# GPU Resource Selection Guide ## GPU Types on ACCESS-CI Systems ### NVIDIA A100 - **Best for**: Large-scale deep learning, large models, transformers - **Memory**: 40GB or 80GB variants - **Key features**: Tensor cores, multi-instance GPU (MIG), NVLink - **Typical systems**: Delta, Bridges-2 ### NVIDIA V100 - **Best for**: General deep learning, scientific computing - **Memory**: 16GB or 32GB variants - **Key features**: Tensor cores, NVLink - **Typical systems**: Bridges-2, Expanse ### NVIDIA A40 - **Best for**: Visualization, rendering, inference - **Memory**: 48GB - **Key features**: RTX cores, large memory - **Typical systems**: Bridges-2 ## Choosing the Right GPU ### For Training Large Language Models - **Recommendation**: A100 80GB - **Rationale**: Large model parameters require high GPU memory - **Alternative**: Multiple A100 40GB with model parallelism ### For Computer Vision (CNNs) - **Recommendation**: A100 40GB or V100 32GB - **Rationale**: Good balance of memory and compute for typical CV workloads ### For Molecular Dynamics - **Recommendation**: A100 or V100 - **Rationale**: MD codes benefit from double-precision performance ### For Inference/Deployment - **Recommendation**: A40 or T4 - **Rationale**: Efficient inference, good price/performance ## Batch Size Considerations **Small models (<100M parameters)**: V100 or A100 40GB sufficient **Medium models (100M-1B parameters)**: A100 40GB recommended **Large models (1B-10B parameters)**: A100 80GB or multi-GPU **Very large models (>10B parameters)**: Multi-GPU A100 80GB required ## Multi-GPU Training Most ACCESS systems support: - **Data parallelism**: Distribute batches across GPUs - **Model parallelism**: Split model layers across GPUs - **Pipeline parallelism**: Different model stages on different GPUs Use **search_resources** with **has_gpu: true** to find GPU-enabled systems. `; return this.createMarkdownResource(uri, guide); } if (uri === "accessci://compute-resources/resource-types") { return this.createJsonResource(uri, { resource_types: RESOURCE_TYPES, usage_notes: { CPU: "Most versatile, suitable for serial and parallel codes", GPU: "Best for highly parallel workloads, requires GPU-optimized code", "High Memory": "Essential for workloads that need >100GB RAM", Storage: "For datasets >10TB or high I/O requirements", Cloud: "Best for web services, containers, variable workloads", }, }); } throw new Error(`Unknown resource: ${uri}`); } async handleGetPrompt(request) { const { name, arguments: args = {} } = request.params; if (name === "recommend_compute_resource") { const { research_area = "", compute_needs = "", allocation_size, experience_level } = args; // Selective context embedding based on user needs const lowerNeeds = compute_needs.toLowerCase(); const lowerResearch = research_area.toLowerCase(); // 1. Find matching field of science for domain context let matchedField = null; for (const [fieldName, fieldData] of Object.entries(FIELDS_OF_SCIENCE)) { const keywords = fieldData.keywords.map((k) => k.toLowerCase()); if (keywords.some((k) => lowerResearch.includes(k) || lowerNeeds.includes(k)) || lowerResearch.includes(fieldName.toLowerCase())) { matchedField = fieldData; break; } } // 2. Determine if GPU is relevant const gpuRelevant = lowerNeeds.includes("gpu") || lowerNeeds.includes("deep learning") || lowerNeeds.includes("machine learning") || lowerNeeds.includes("ai") || lowerNeeds.includes("neural network") || lowerResearch.includes("machine learning") || lowerResearch.includes("computer vision"); // 3. Determine memory requirements const highMemoryRelevant = lowerNeeds.includes("memory") || lowerNeeds.includes("genome") || lowerNeeds.includes("assembly") || lowerNeeds.includes("graph"); // 4. Filter relevant systems based on needs const relevantSystems = {}; for (const [name, system] of Object.entries(ACCESS_SYSTEMS)) { const isRelevant = (gpuRelevant && system.gpu_types && system.gpu_types.length > 0) || (highMemoryRelevant && system.strengths.some((s) => s.includes("Memory"))) || (!gpuRelevant && !highMemoryRelevant); // Include general-purpose systems // Filter by experience level if specified const experienceMatch = !experience_level || system.experience_level.some((level) => level.toLowerCase().includes(experience_level.toLowerCase())); if (isRelevant && experienceMatch) { relevantSystems[name] = system; } } // 5. Build compact context sections const contextSections = []; // Field of science context if (matchedField) { contextSections.push(`**Your Research Field**: ${matchedField.name} - Typical Resources: ${matchedField.typical_resources.join(", ")} - Common Software: ${matchedField.common_software.slice(0, 6).join(", ")} - Typical Allocation: ${matchedField.allocation_range?.min.toLocaleString()} - ${matchedField.allocation_range?.max.toLocaleString()} ACCESS Credits`); } // Relevant ACCESS systems contextSections.push(`**Relevant ACCESS Systems**: ${Object.entries(relevantSystems) .map(([name, sys]) => `- **${name}** (${sys.organization}): ${sys.description} ${sys.gpu_types ? `GPUs: ${sys.gpu_types.join(", ")}` : ""} ${sys.max_memory_per_node ? `Max Memory: ${sys.max_memory_per_node}` : ""} Ideal for: ${sys.ideal_for.slice(0, 3).join(", ")}`) .join("\n\n")}`); // GPU guide if relevant if (gpuRelevant) { const relevantGpuGuides = Object.entries(GPU_SELECTION_GUIDE).filter(([useCase]) => lowerNeeds.includes(useCase.toLowerCase()) || lowerResearch.includes(useCase.toLowerCase())); if (relevantGpuGuides.length > 0) { contextSections.push(`**GPU Guidance**: ${relevantGpuGuides .map(([useCase, guide]) => `- **${useCase}**: ${guide.recommended_gpu} Systems: ${guide.recommended_systems.join(", ")} ${guide.notes}`) .join("\n\n")}`); } } // Memory requirements if relevant if (highMemoryRelevant) { contextSections.push(`**Memory Options**: ${Object.entries(MEMORY_REQUIREMENTS) .map(([range, info]) => `- **${range}**: ${info.description} Systems: ${info.recommended_systems.join(", ")}`) .join("\n")}`); } // 6. Construct the final prompt const promptText = `I need help selecting appropriate ACCESS-CI compute resources for my research. **Research Area**: ${research_area} **Computational Needs**: ${compute_needs} ${experience_level ? `**Experience Level**: ${experience_level}` : ""} ${allocation_size ? `**Allocation Size**: ${allocation_size}` : ""} ${contextSections.join("\n\n")} --- Based on this context and my requirements, please: 1. **Recommend 2-3 specific ACCESS systems** that best match my needs 2. **Explain the rationale** for each recommendation 3. **Suggest an allocation tier** (Discover: 1K-400K, Explore: 400K-1.5M, Accelerate: 1.5M-10M, Maximize: 10M+ credits) 4. **Recommend next steps** for getting started${experience_level === "beginner" ? ", keeping in mind I'm new to HPC" : ""} Consider: - Which resource types (CPU/GPU/Memory/Storage) match my computational pattern - Which systems have the specific capabilities I need - What allocation size makes sense for my project scale - Any relevant software that might be pre-installed`; return { description: `Personalized compute resource recommendation for ${research_area}`, messages: [ { role: "user", content: { type: "text", text: promptText, }, }, ], }; } throw new Error(`Unknown prompt: ${name}`); } /** * Static mapping of known organization IDs to names. * This serves as a fallback when the API doesn't return organization info. * * These names are fetched from /wh2/cider/v1/organizations/ endpoint. * Mapping updated: 2025-10-21 via scripts/fetch-organizations-working-endpoint.js * * Note: API should be preferred (see listComputeResources method), this is only * used if API fetch fails. Allows new organizations to be discovered automatically. */ KNOWN_ORGANIZATIONS = { // Major HPC Centers 844: "National Center for Supercomputing Applications", 856: "San Diego Supercomputer Center", 848: "Pittsburgh Supercomputing Center", 2058: "Texas Advanced Computing Center", // Universities 467: "Texas A&M University", 476: "University of Texas at Austin", 561: "Indiana University", 563: "University of Kentucky", 1869: "Purdue University", 178: "Northwestern University", 471: "Texas Tech University", 14449: "Institute for Advanced Computational Science at Stony Brook University", // Research Infrastructure & Projects 2000: "Renaissance Computing Institute", 653: "NSF National Center for Atmospheric Research", 2440: "OSG Consortium", 4659: "Science Gateways Center of Excellence", 12964: "Open Storage Network", 16235: "ACCESS Support", 19169: "CloudBank", }; /** * Common organization abbreviations for better search UX * Includes HPC centers, universities, and research institutions */ ORG_ABBREVIATIONS = { // Major HPC/Supercomputing Centers NCSA: ["National Center for Supercomputing Applications", "Illinois"], SDSC: ["San Diego Supercomputer Center"], PSC: ["Pittsburgh Supercomputing Center"], TACC: ["Texas Advanced Computing Center"], NCAR: [ "NSF National Center for Atmospheric Research", "National Center for Atmospheric Research", ], NERSC: ["National Energy Research Scientific Computing Center"], ALCF: ["Argonne Leadership Computing Facility"], OLCF: ["Oak Ridge Leadership Computing Facility"], // Universities (common abbreviations) IU: ["Indiana University"], UK: ["University of Kentucky"], TAMU: ["Texas A&M University", "Texas A&M"], UT: ["University of Texas at Austin"], TTU: ["Texas Tech University"], OSU: ["Ohio State University"], ASU: ["Arizona State University"], FSU: ["Florida State University"], PSU: ["Pennsylvania State University", "Penn State"], MSU: ["Michigan State University"], USC: ["University of Southern California"], UCLA: ["University of California, Los Angeles"], UIUC: ["University of Illinois at Urbana-Champaign", "Illinois"], MIT: ["Massachusetts Institute of Technology"], CU: ["University of Colorado"], "CU Boulder": ["University of Colorado Boulder"], UChicago: ["University of Chicago"], // Research Computing Consortia & Networks OSG: ["OSG Consortium", "Open Science Grid"], OSN: ["Open Storage Network"], SGCI: ["Science Gateways Center of Excellence", "Science Gateways Community Institute"], RENCI: ["Renaissance Computing Institute"], ACCESS: ["ACCESS Support", "Advanced Cyberinfrastructure Coordination Ecosystem"], XSEDE: ["Extreme Science and Engineering Discovery Environment"], // Legacy }; async listComputeResources() { // Get all active resource groups const response = await this.httpClient.get("/wh2/cider/v1/access-active-groups/type/resource-catalog.access-ci.org/"); // Also try to get organization information const orgMapping = new Map(); // First, add known static mappings as fallback Object.entries(this.KNOWN_ORGANIZATIONS).forEach(([id, name]) => { orgMapping.set(parseInt(id), name); }); // Then try to fetch live organization data (will override static mappings) // NOTE: Using /organizations/ endpoint (not /access-active-groups/type/organizations.access-ci.org/ // which returns 0 results). See ACCESS_CI_API_ISSUE_ORGANIZATIONS.md for details. try { const orgResponse = await this.httpClient.get("/wh2/cider/v1/organizations/"); if (orgResponse.status === 200 && orgResponse.data?.results) { orgResponse.data.results.forEach((org) => { if (org.organization_id && org.organization_name) { orgMapping.set(org.organization_id, org.organization_name); } }); } } catch (e) { // If organizations endpoint fails, we'll use the static mapping console.warn("Could not fetch organization names from API, using fallback mapping"); } // Check if the response has the expected structure if (!response.data || !response.data.results || !response.data.results.active_groups) { throw new Error(`Unexpected API response structure. Got: ${JSON.stringify(response.data)}`); } const computeResources = response.data.results.active_groups .filter((group) => { // Filter for compute resources (category 1 = "Compute & Storage Resources") return (group.rollup_info_resourceids && group.rollup_feature_ids && !group.rollup_feature_ids.includes(137)); }) .map((group) => { // Map organization IDs to names if available const organizationNames = (group.rollup_organization_ids || []).map((id) => orgMapping.get(id) || id.toString()); return { id: group.info_groupid, name: group.group_descriptive_name, description: group.group_description, organization_ids: group.rollup_organization_ids, organization_names: organizationNames, features: group.rollup_feature_ids, feature_names: getFeatureNames(group.rollup_feature_ids || []), // NEW: Human-readable feature names resources: group.rollup_info_resourceids, logoUrl: group.group_logo_url, accessAllocated: group.rollup_feature_ids?.includes(139) ?? false, // Add computed fields for easier filtering hasGpu: this.detectGpuCapability(group), resourceType: this.determineResourceType(group), // Include the actual resource IDs that other ACCESS-CI services can use resourceIds: group.rollup_info_resourceids || [], }; }); return { content: [ { type: "text", text: JSON.stringify({ total: computeResources.length, items: computeResources, }), }, ], }; } async getComputeResource(resourceId) { const sanitizedId = sanitizeGroupId(resourceId); // Get detailed resource information const response = await this.httpClient.get(`/wh2/cider/v1/access-active/info_groupid/${sanitizedId}/?format=json`); // Check for errors if (response.status !== 200) { return this.errorResponse(`Resource not found: '${resourceId}'`, "Use 'search_resources' to find valid resource IDs. Resource IDs typically look like 'delta.ncsa.access-ci.org' or 'bridges2.psc.access-ci.org'"); } // Check if results exist and are valid if (!response.data || !response.data.results || response.data.results.length === 0) { return this.errorResponse(`Resource not found: '${resourceId}'`, "Use 'search_resources' to find valid resource IDs"); } return { content: [ { type: "text", text: JSON.stringify(response.data.results, null, 2), }, ], }; } async getResourceHardware(inputId) { // Resolve human-readable name to full resource ID if needed const resolved = await resolveResourceId(inputId, async (query) => { const result = await this.searchResources({ query }); const content = result.content[0]; if (content.type !== "text") return []; const data = JSON.parse(content.text); return (data.items || []).map((item) => ({ id: item.id || "", name: item.name || "", })); }); if (!resolved.success) { return this.errorResponse(resolved.error, resolved.suggestion); } const resourceId = resolved.id; const resourceData = await this.getComputeResource(resourceId); // Check if getComputeResource returned an error // Parse the content to check if it's an error response const firstContent = resourceData.content[0]; if (firstContent.type !== "text") { return resourceData; // Return as-is if not text content } const parsedData = JSON.parse(firstContent.text); if (parsedData.error) { return resourceData; // Return the error response as-is } // Extract hardware-related information const fullData = parsedData; const hardwareInfo = fullData.filter((item) => item.cider_type === "Compute" || item.cider_type === "Storage" || item.resource_descriptive_name?.toLowerCase().includes("node") || item.resource_descriptive_name?.toLowerCase().includes("core") || item.resource_descriptive_name?.toLowerCase().includes("memory") || item.resource_descriptive_name?.toLowerCase().includes("gpu")); // Structure the hardware data for easier consumption const structuredHardware = this.structureHardwareInfo(hardwareInfo); return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, hardware: structuredHardware, raw_hardware_items: hardwareInfo, // Keep raw data for reference documentation: { note: "Hardware specifications are derived from the resource catalog API", details_url: `https://operations-api.access-ci.org/wh2/cider/v1/access-active/info_groupid/${resourceId}`, }, }, null, 2), }, ], }; } /** * Add contextual documentation links - only included when genuinely helpful * * @param context - What operation is being performed ('list' | 'search' | 'details') */ addDocumentation(context = "list") { // For listing resources, provide next-step links if (context === "list") { return { next_steps: "https://allocations.access-ci.org/get-started", resource_catalog: "https://allocations.access-ci.org/resources", }; } // For search results, documentation is less useful - users already know what they want if (context === "search") { return undefined; // Don't clutter search results } // For resource details, provide specific resource documentation return undefined; // Resource-specific docs should come from the resource itself } /** * Structure raw hardware items into organized categories */ structureHardwareInfo(hardwareItems) { const structured = { compute_nodes: [], storage: [], gpus: [], memory: [], other: [], }; for (const item of hardwareItems) { const name = item.resource_descriptive_name?.toLowerCase() || ""; const type = item.cider_type; // Categorize by type and name const entry = { name: item.resource_descriptive_name || "", type: item.cider_type || "", details: item.resource_description || "", }; if (name.includes("gpu") || name.includes("accelerator")) { structured.gpus.push(entry); } else if (name.includes("memory") || name.includes("ram")) { structured.memory.push(entry); } else if (type === "Storage" || name.includes("storage") || name.includes("disk")) { structured.storage.push(entry); } else if (type === "Compute" || name.includes("node") || name.includes("core") || name.includes("cpu")) { structured.compute_nodes.push(entry); } else { structured.other.push(entry); } } // Remove empty categories and return const result = {}; for (const [key, value] of Object.entries(structured)) { if (value.length > 0) { result[key] = value; } } return result; } detectGpuCapability(group) { // Check if the group has GPU-related features or descriptions const description = (group.group_description || "").toLowerCase(); const name = (group.group_descriptive_name || "").toLowerCase(); return (description.includes("gpu") || description.includes("graphics") || name.includes("gpu") || name.includes("delta") || // Delta is known GPU system (group.rollup_feature_ids?.includes(142) ?? false) // GPU feature ID (if exists) ); } determineResourceType(group) { const description = (group.group_description || "").toLowerCase(); const name = (group.group_descriptive_name || "").toLowerCase(); if (description.includes("cloud") || name.includes("jetstream")) { return "cloud"; } if (this.detectGpuCapability(group)) { return "gpu"; } if (description.includes("storage")) { return "storage"; } return "compute"; } async searchResources(args) { const { query, resource_type, has_gpu, organization, include_resource_ids = true } = args; // Get all resources first const allResourcesResult = await this.listComputeResources(); const firstContent = allResourcesResult.content[0]; if (firstContent.type !== "text") { return allResourcesResult; // Return as-is if not text content } const allResourcesData = JSON.parse(firstContent.text); let resources = allResourcesData.items || []; // Apply filters if (query) { const searchTerm = query.toLowerCase(); resources = resources.filter((resource) => resource.name?.toLowerCase().includes(searchTerm) || resource.description?.toLowerCase().includes(searchTerm) || (Array.isArray(resource.organization_names) && resource.organization_names.some((org) => typeof org === "string" && org.toLowerCase().includes(searchTerm)))); } if (resource_type) { resources = resources.filter((resource) => resource.resourceType === resource_type); } // Only filter by GPU when has_gpu is explicitly true or false (not null/undefined) if (has_gpu === true || has_gpu === false) { resources = resources.filter((resource) => resource.hasGpu === has_gpu); } // Organization filter (ENHANCED: works with partial names, abbreviations, and searches known orgs) if (organization) { const orgLower = organization.toLowerCase(); const orgUpper = organization.toUpperCase(); // Check if input is an abbreviation and expand it to full names const searchTerms = [orgLower]; if (this.ORG_ABBREVIATIONS[orgUpper]) { searchTerms.push(...this.ORG_ABBREVIATIONS[orgUpper].map((n) => n.toLowerCase())); } // Find matching organization IDs from known organizations using all search terms const matchingKnownOrgIds = Object.entries(this.KNOWN_ORGANIZATIONS) .filter(([_, name]) => searchTerms.some((term) => name.toLowerCase().includes(term))) .map(([id, _]) => parseInt(id)); resources = resources.filter((resource) => { if (!Array.isArray(resource.organization_names)) { return false; } // Check if any organization name matches any of our search terms const nameMatch = resource.organization_names.some((org) => typeof org === "string" && searchTerms.some((term) => org.toLowerCase().includes(term))); if (nameMatch) { return true; } // Also check if the organization IDs match known organizations // (This handles cases where organization_names contains IDs as strings) if (matchingKnownOrgIds.length > 0 && resource.organization_ids) { const hasMatchingId = matchingKnownOrgIds.some((knownId) => resource.organization_ids?.includes(knownId)); if (hasMatchingId) { return true; } } return false; }); } // Add resource IDs if requested if (include_resource_ids) { resources = resources.map((resource) => ({ ...resource, resource_ids: resource.resourceIds, })); } return { content: [ { type: "text", text: JSON.stringify({ total: resources.length, items: resources, }), }, ], }; } }