@access-mcp/compute-resources
Version:
MCP server for ACCESS-CI Compute Resources API
299 lines (298 loc) • 13.5 kB
JavaScript
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),
},
],
};
}
}