UNPKG

@access-mcp/software-discovery

Version:

ACCESS-CI Software Discovery Service MCP server

373 lines (372 loc) 14.8 kB
import { BaseAccessServer, handleApiError } from "@access-mcp/shared"; import axios from "axios"; export class SoftwareDiscoveryServer extends BaseAccessServer { _sdsClient; constructor() { super("access-mcp-software-discovery", "0.3.0", "https://ara-db.ccs.uky.edu"); } /** * Normalizes resource IDs to handle legacy XSEDE format and domain variations. * This provides backward compatibility while the SDS API migrates to ACCESS-CI format. * * @param resourceId - The resource ID to normalize * @returns The normalized resource ID in ACCESS-CI format */ normalizeResourceId(resourceId) { // Convert old XSEDE format to new ACCESS-CI format if (resourceId.includes('.xsede.org')) { return resourceId.replace('.xsede.org', '.access-ci.org'); } // Convert legacy domain variations if (resourceId.includes('.illinois.edu')) { return resourceId.replace('.illinois.edu', '.access-ci.org'); } if (resourceId.includes('.edu')) { return resourceId.replace('.edu', '.access-ci.org'); } // If already in correct format or unknown format, return as-is return resourceId; } get sdsClient() { if (!this._sdsClient) { this._sdsClient = axios.create({ baseURL: "https://ara-db.ccs.uky.edu", timeout: 10000, headers: { "User-Agent": "access-mcp-software-discovery/0.3.0", }, validateStatus: () => true, }); } return this._sdsClient; } getTools() { return [ { name: "search_software", description: "Search for software packages across ACCESS-CI resources", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for software names or descriptions", }, resource_filter: { type: "string", description: "Optional: filter results by specific resource ID", }, }, required: ["query"], }, }, { name: "list_software_by_resource", description: "List all available software packages for a specific ACCESS-CI resource", inputSchema: { type: "object", properties: { resource_id: { type: "string", description: "The resource ID (e.g., anvil.purdue.access-ci.org)", }, limit: { type: "number", description: "Maximum number of packages to return (default: 100)", }, }, required: ["resource_id"], }, }, { name: "get_software_details", description: "Get detailed information about a specific software package", inputSchema: { type: "object", properties: { software_name: { type: "string", description: "Name of the software package", }, resource_id: { type: "string", description: "Optional: specific resource to get package details for", }, }, required: ["software_name"], }, }, { name: "get_software_categories", description: "Get available software categories and domains", inputSchema: { type: "object", properties: { resource_id: { type: "string", description: "Optional: filter categories by specific resource", }, }, required: [], }, }, ]; } getResources() { return [ { uri: "accessci://software-discovery", name: "ACCESS-CI Software Discovery Service", description: "Search and discover software packages available on ACCESS-CI resources", mimeType: "application/json", }, { uri: "accessci://software/categories", name: "Software Categories", description: "Browse software by category and domain", mimeType: "application/json", }, ]; } async handleToolCall(request) { const { name, arguments: args = {} } = request.params; try { switch (name) { case "search_software": return await this.searchSoftware(args.query, args.resource_filter); case "list_software_by_resource": return await this.listSoftwareByResource(args.resource_id, args.limit); case "get_software_details": return await this.getSoftwareDetails(args.software_name, args.resource_id); case "get_software_categories": return await this.getSoftwareCategories(args.resource_id); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${handleApiError(error)}`, }, ], }; } } async handleResourceRead(request) { const { uri } = request.params; switch (uri) { case "accessci://software-discovery": return { contents: [ { uri, mimeType: "text/plain", text: "ACCESS-CI Software Discovery Service - Search and discover software packages available on ACCESS-CI resources.", }, ], }; case "accessci://software/categories": const categories = await this.getSoftwareCategories(); return { contents: [ { uri, mimeType: "application/json", text: categories.content[0].text, }, ], }; default: throw new Error(`Unknown resource: ${uri}`); } } async searchSoftware(query, resourceFilter) { const apiKey = process.env.SDS_API_KEY || process.env.VITE_SDS_API_KEY; if (!apiKey) { return { content: [ { type: "text", text: JSON.stringify({ error: "SDS API key not configured. Set SDS_API_KEY environment variable.", results: [], }, null, 2), }, ], }; } // For search, we'll need to query multiple resources or use a search endpoint // For now, implement basic search by getting software for a specific resource if provided if (resourceFilter) { return await this.listSoftwareByResource(resourceFilter, 100, query); } return { content: [ { type: "text", text: JSON.stringify({ message: "Global software search requires a resource_filter parameter for now. Use list_software_by_resource with a specific resource ID.", query, results: [], }, null, 2), }, ], }; } async listSoftwareByResource(resourceId, limit = 100, searchQuery) { const apiKey = process.env.SDS_API_KEY || process.env.VITE_SDS_API_KEY; if (!apiKey) { return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, error: "SDS API key not configured. Set SDS_API_KEY environment variable.", software: [], }, null, 2), }, ], }; } // Normalize the resource ID to handle legacy formats const normalizedResourceId = this.normalizeResourceId(resourceId); const apiFields = [ "software_name", "software_description", "software_web_page", "software_documentation", "software_use_link", "software_versions", ]; const response = await this.sdsClient.get(`/api=API_0/${apiKey}/rp=${normalizedResourceId}?include=${apiFields.join(",")}`); if (response.status !== 200) { return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, normalized_resource_id: normalizedResourceId, error: `SDS API error: ${response.status} ${response.statusText}`, software: [], }, null, 2), }, ], }; } let softwareList = Array.isArray(response.data) ? response.data : []; // Apply search filter if provided if (searchQuery) { const query = searchQuery.toLowerCase(); softwareList = softwareList.filter((pkg) => pkg.software_name?.toLowerCase().includes(query) || pkg.software_description?.toLowerCase().includes(query)); } // Apply limit if (limit && softwareList.length > limit) { softwareList = softwareList.slice(0, limit); } return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, normalized_resource_id: normalizedResourceId, total_packages: softwareList.length, search_query: searchQuery, software: softwareList.map((pkg) => ({ name: pkg.software_name, description: pkg.software_description, versions: pkg.software_versions || [], documentation: pkg.software_documentation, website: pkg.software_web_page, usage_link: pkg.software_use_link, })), }, null, 2), }, ], }; } async getSoftwareDetails(softwareName, resourceId) { if (!resourceId) { return { content: [ { type: "text", text: JSON.stringify({ software_name: softwareName, error: "resource_id parameter is required to get software details", details: null, }, null, 2), }, ], }; } // Get all software for the resource and filter by name const allSoftware = await this.listSoftwareByResource(resourceId, 1000); const allSoftwareData = JSON.parse(allSoftware.content[0].text); if (allSoftwareData.error) { return allSoftware; } const softwareDetails = allSoftwareData.software.find((pkg) => pkg.name.toLowerCase() === softwareName.toLowerCase()); return { content: [ { type: "text", text: JSON.stringify({ software_name: softwareName, resource_id: resourceId, found: !!softwareDetails, details: softwareDetails || null, }, null, 2), }, ], }; } async getSoftwareCategories(resourceId) { // This would ideally query the SDS API for categories // For now, return common software categories found on HPC systems const categories = [ { name: "Compilers", description: "Programming language compilers and toolchains", }, { name: "Libraries", description: "Software libraries and frameworks" }, { name: "Applications", description: "End-user applications and tools" }, { name: "Development Tools", description: "Development and debugging tools", }, { name: "Scientific Computing", description: "Scientific and numerical computing packages", }, { name: "Data Analytics", description: "Data analysis and visualization tools", }, { name: "Machine Learning", description: "AI and machine learning frameworks", }, { name: "Bioinformatics", description: "Biological data analysis tools" }, { name: "Chemistry", description: "Computational chemistry packages" }, { name: "Physics", description: "Physics simulation and modeling tools" }, ]; return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, categories: categories, }, null, 2), }, ], }; } }