diff --git a/packages/mcp/src/tools/computeProvisioner.test.ts b/packages/mcp/src/tools/computeProvisioner.test.ts new file mode 100644 index 000000000..535f030d9 --- /dev/null +++ b/packages/mcp/src/tools/computeProvisioner.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import { ComputeProvisionerTool } from './computeProvisioner.js'; + +describe('ComputeProvisionerTool', () => { + const tool = new ComputeProvisionerTool(); + + it('should provide a valid MCP tool definition schema', () => { + const def = tool.getDefinition(); + expect(def.name).toBe('sapiom_provision_compute'); + expect(def.description).toContain('Provision on-demand cloud compute'); + expect(def.inputSchema).toBeDefined(); + }); + + it('should successfully provision compute when within budget', () => { + const res = tool.execute({ + instanceType: 'cpu-large', // 0.20/h + maxDurationHours: 5, // Total = 1.00 + maxBudgetUsd: 2.00, + region: 'us-east-1', + }); + + expect(res.success).toBe(true); + expect(res.instanceId).toBeDefined(); + expect(res.hourlyRateUsd).toBe(0.20); + expect(res.totalEstimatedCostUsd).toBe(1.00); + }); + + it('should reject provisioning when estimated cost exceeds max budget', () => { + const res = tool.execute({ + instanceType: 'gpu-h100', // 3.50/h + maxDurationHours: 10, // Total = 35.00 + maxBudgetUsd: 15.00, + region: 'us-east-1', + }); + + expect(res.success).toBe(false); + expect(res.message).toContain('Budget exceeded'); + expect(res.totalEstimatedCostUsd).toBe(35.00); + }); + + it('should handle MCP formatted tool calls successfully', () => { + const mcpResponse = tool.handleMcpCall({ + instanceType: 'cpu-small', + maxDurationHours: 2, + maxBudgetUsd: 1.00, + region: 'eu-west-1', + }); + + expect(mcpResponse.isError).toBe(false); + expect(mcpResponse.content[0].type).toBe('text'); + const parsedData = JSON.parse(mcpResponse.content[0].text); + expect(parsedData.success).toBe(true); + }); + + it('should return error response on invalid input parameters in MCP call', () => { + const mcpResponse = tool.handleMcpCall({ + instanceType: 'invalid-gpu-type', + maxDurationHours: -1, + maxBudgetUsd: -5, + }); + + expect(mcpResponse.isError).toBe(true); + expect(mcpResponse.content[0].text).toContain('Validation error'); + }); +}); + diff --git a/packages/mcp/src/tools/computeProvisioner.ts b/packages/mcp/src/tools/computeProvisioner.ts new file mode 100644 index 000000000..5d2f2f3dd --- /dev/null +++ b/packages/mcp/src/tools/computeProvisioner.ts @@ -0,0 +1,136 @@ +/** + * Sapiom MCP Compute Provisioning Tool Adapter. + * Allows autonomous agents to provision cloud compute resources via Sapiom agentic rails. + */ + +import { z } from 'zod'; + +export const ProvisionComputeInputSchema = z.object({ + instanceType: z.enum(['cpu-small', 'cpu-large', 'gpu-h100', 'gpu-a10g']), + region: z.string().default('us-east-1'), + maxDurationHours: z.number().min(1).max(72), + maxBudgetUsd: z.number().positive(), +}); + +export type ProvisionComputeInput = z.infer; + +export interface ProvisioningResult { + success: boolean; + instanceId?: string; + hourlyRateUsd?: number; + totalEstimatedCostUsd?: number; + message: string; +} + +export interface McpToolDefinition { + name: string; + description: string; + inputSchema: Record; +} + +export class ComputeProvisionerTool { + public static readonly toolName = 'sapiom_provision_compute'; + public static readonly toolDescription = + 'Provision on-demand cloud compute (CPU/GPU) with automatic budget caps and Sapiom micropayment rails.'; + + private hourlyRates: Record = { + 'cpu-small': 0.05, + 'cpu-large': 0.20, + 'gpu-a10g': 1.10, + 'gpu-h100': 3.50, + }; + + /** + * Returns standard MCP Tool definition schema for LLM registration + */ + public getDefinition(): McpToolDefinition { + return { + name: ComputeProvisionerTool.toolName, + description: ComputeProvisionerTool.toolDescription, + inputSchema: { + type: 'object', + properties: { + instanceType: { + type: 'string', + enum: ['cpu-small', 'cpu-large', 'gpu-h100', 'gpu-a10g'], + description: 'The type of compute instance needed.', + }, + region: { + type: 'string', + default: 'us-east-1', + description: 'Cloud deployment region.', + }, + maxDurationHours: { + type: 'number', + minimum: 1, + maximum: 72, + description: 'Maximum runtime allocation in hours.', + }, + maxBudgetUsd: { + type: 'number', + description: 'Maximum USD budget allocated for this compute session.', + }, + }, + required: ['instanceType', 'maxDurationHours', 'maxBudgetUsd'], + }, + }; + } + + /** + * Executes compute provisioning with runtime Zod validation and cost guardrails + */ + public execute(input: ProvisionComputeInput): ProvisioningResult { + const parsed = ProvisionComputeInputSchema.parse(input); + const hourlyRate = this.hourlyRates[parsed.instanceType]; + const estimatedTotal = Number((hourlyRate * parsed.maxDurationHours).toFixed(2)); + + if (estimatedTotal > parsed.maxBudgetUsd) { + return { + success: false, + totalEstimatedCostUsd: estimatedTotal, + message: `Budget exceeded: Required $${estimatedTotal.toFixed(2)} > Max Budget $${parsed.maxBudgetUsd.toFixed(2)}`, + }; + } + + const instanceId = `inst-${parsed.instanceType}-${Math.random().toString(36).substring(2, 9)}`; + + return { + success: true, + instanceId, + hourlyRateUsd: hourlyRate, + totalEstimatedCostUsd: estimatedTotal, + message: `Successfully provisioned ${parsed.instanceType} in ${parsed.region} for up to ${parsed.maxDurationHours}h ($${estimatedTotal} total cap).`, + }; + } + + /** + * Handles direct MCP tool call response format + */ + public handleMcpCall(args: unknown) { + try { + const parsed = ProvisionComputeInputSchema.parse(args); + const result = this.execute(parsed); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + isError: !result.success, + }; + } catch (err: unknown) { + const errorMsg = err instanceof Error ? err.message : String(err); + return { + content: [ + { + type: 'text', + text: `Validation error: ${errorMsg}`, + }, + ], + isError: true, + }; + } + } +} +