UNPKG

@access-mcp/compute-resources

Version:

MCP server for ACCESS-CI Compute Resources API

299 lines (298 loc) 13.5 kB
import { BaseAccessServer, handleApiError, sanitizeGroupId, } from "@access-mcp/shared"; export class ComputeResourcesServer extends BaseAccessServer { constructor() { super("access-mcp-compute-resources", "0.3.0", "https://operations-api.access-ci.org"); } getTools() { return [ { name: "list_compute_resources", description: "List all ACCESS-CI compute resources", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_compute_resource", description: "Get detailed information about a specific compute resource", inputSchema: { type: "object", properties: { resource_id: { type: "string", description: "The resource ID or info_groupid", }, }, required: ["resource_id"], }, }, { name: "get_resource_hardware", description: "Get hardware specifications for a compute resource", inputSchema: { type: "object", properties: { resource_id: { type: "string", description: "The resource ID or info_groupid", }, }, required: ["resource_id"], }, }, { name: "search_resources", description: "**CRITICAL TOOL**: Search for compute resources and discover resource IDs needed by other ACCESS-CI services. **IMPORTANT**: This tool is essential for discovering resource IDs required by software-discovery, system-status, and other ACCESS-CI tools. Always use `include_resource_ids: true` when you need IDs for other operations.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search term to match against resource names, descriptions, and organizations", }, resource_type: { type: "string", enum: ["compute", "storage", "cloud", "gpu", "cpu"], description: "Filter by resource type", }, has_gpu: { type: "boolean", description: "Filter for resources with GPU capabilities", }, include_resource_ids: { type: "boolean", description: "**CRITICAL**: Include resource IDs needed for other ACCESS-CI services. These IDs are required parameters for software discovery, system status, and other ACCESS-CI tools. Always set to true when you need resource IDs for subsequent operations. **Common Workflow**: 1) Search resources with this enabled, 2) Use returned IDs in other ACCESS-CI tools.", }, }, }, }, ]; } getResources() { return [ { uri: "accessci://compute-resources", name: "ACCESS-CI Compute Resources", description: "Information about ACCESS-CI compute resources, hardware, and software", mimeType: "application/json", }, ]; } async handleToolCall(request) { const { name, arguments: args } = request.params; try { switch (name) { case "list_compute_resources": return await this.listComputeResources(); case "get_compute_resource": return await this.getComputeResource(args.resource_id); case "get_resource_hardware": return await this.getResourceHardware(args.resource_id); case "search_resources": return await this.searchResources(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${handleApiError(error)}`, }, ], }; } } async handleResourceRead(request) { const { uri } = request.params; if (uri === "accessci://compute-resources") { return { contents: [ { uri, mimeType: "text/plain", text: "ACCESS-CI Compute Resources API - Use the available tools to query compute resources, hardware specifications, and software availability.", }, ], }; } throw new Error(`Unknown resource: ${uri}`); } 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 let orgMapping = new Map(); try { const orgResponse = await this.httpClient.get("/wh2/cider/v1/access-active-groups/type/organizations.access-ci.org/"); if (orgResponse.status === 200 && orgResponse.data?.results?.active_groups) { orgResponse.data.results.active_groups.forEach((org) => { if (org.info_groupid && org.group_descriptive_name) { orgMapping.set(org.info_groupid, org.group_descriptive_name); } }); } } catch (e) { // If organizations endpoint doesn't exist, continue with IDs only } // 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.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, resources: group.rollup_info_resourceids, logoUrl: group.group_logo_url, accessAllocated: group.rollup_feature_ids.includes(139), // 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, resources: computeResources, }, null, 2), }, ], }; } 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`); return { content: [ { type: "text", text: JSON.stringify(response.data.results, null, 2), }, ], }; } async getResourceHardware(resourceId) { // For now, return hardware info from the detailed resource endpoint // This could be enhanced with dedicated hardware endpoints if available const resourceData = await this.getComputeResource(resourceId); // Extract hardware-related information const fullData = JSON.parse(resourceData.content[0].text); 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")); return { content: [ { type: "text", text: JSON.stringify({ resource_id: resourceId, hardware: hardwareInfo, }, null, 2), }, ], }; } 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) // 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, include_resource_ids = true } = args; // Get all resources first const allResourcesResult = await this.listComputeResources(); const allResourcesData = JSON.parse(allResourcesResult.content[0].text); let resources = allResourcesData.resources || []; // 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); } if (has_gpu !== undefined) { resources = resources.filter((resource) => resource.hasGpu === has_gpu); } // Add detailed resource ID information if requested if (include_resource_ids) { resources = resources.map((resource) => ({ ...resource, service_integration: { resource_ids: resource.resourceIds, available_for_services: resource.resourceIds.length > 0, usage_note: resource.resourceIds.length > 0 ? `Use any of these IDs with other ACCESS-CI services: ${resource.resourceIds.join(', ')}` : 'No resource IDs available for service integration', }, })); } return { content: [ { type: "text", text: JSON.stringify({ search_criteria: { query, resource_type, has_gpu, include_resource_ids }, total_found: resources.length, resources: resources, usage_notes: include_resource_ids ? { service_integration: "Use any ID from the 'resource_ids' array as 'resource_id' parameter in other ACCESS-CI tools", note: "These are the actual resource IDs from the ACCESS-CI operations API", } : undefined, }, null, 2), }, ], }; } }