Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/mcp/src/tools/computeProvisioner.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

136 changes: 136 additions & 0 deletions packages/mcp/src/tools/computeProvisioner.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ProvisionComputeInputSchema>;

export interface ProvisioningResult {
success: boolean;
instanceId?: string;
hourlyRateUsd?: number;
totalEstimatedCostUsd?: number;
message: string;
}

export interface McpToolDefinition {
name: string;
description: string;
inputSchema: Record<string, unknown>;
}

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<string, number> = {
'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,
};
}
}
}