@net3/queuer
Version:
476 lines (438 loc) • 17 kB
text/typescript
import {
createAction,
DynamicPropsValue,
Property,
ActionContext,
StoreScope,
} from '@activepieces/pieces-framework';
import { QueueManager } from '../common/queue-manager';
import { MCPManager } from '../common/mcp-manager';
import { QueueConfiguration, QueueState } from '../common/types';
/**
* Action: Create/Update Queue
* Builds or updates a queue configuration and returns current status.
* Now uses MCP tools instead of regular pieces.
*/
export const createUpdateQueue = createAction({
name: 'create_update_queue',
displayName: 'Create/Update Queue',
description: 'Create a new queue or update existing queue configuration for MCP tools',
props: {
/* -------------------- MCP Configuration -------------------- */
targetMCPTool: Property.Dropdown({
displayName: 'MCP Tool',
description: 'The MCP tool that all items in this queue will use',
required: true,
refreshers: [],
options: async ({ auth }) => {
const authConfig = auth as { mcpServerUrl?: string };
if (!authConfig?.mcpServerUrl) {
return {
options: [],
placeholder: 'MCP Server URL required in auth configuration'
};
}
try {
const config = { mcpServerUrl: authConfig.mcpServerUrl };
const tools = await MCPManager.listMCPTools(config);
if (!tools || tools.length === 0) {
return {
options: [],
placeholder: 'No MCP tools available from server'
};
}
return {
options: tools.map((tool: any) => ({
label: `${tool.name} - ${tool.description || 'No description'}`,
value: tool.name,
}))
};
} catch (error: any) {
console.error('Failed to fetch MCP tools:', error);
return {
options: [],
disabled: true,
placeholder: `Error: ${error.message || 'Failed to connect to MCP server'}`
};
}
},
}),
queueSettings: Property.DynamicProperties({
displayName: 'Queue Settings',
description: 'Configure queue settings (auto-loads existing settings if queue exists)',
required: false,
refreshers: ['targetMCPTool'],
props: async ({ targetMCPTool, auth }, context) => {
// Default values for new queue
const defaults = {
delayType: 'fixed',
delayUnit: 'seconds',
delayValue: 30,
delayMin: 20,
delayMax: 60,
dailyLimit: 100,
hourlyLimit: 20,
activeHours: {
timezone: 'America/New_York',
schedule: {
monday: { enabled: true, start: '09:00', end: '17:00' },
tuesday: { enabled: true, start: '09:00', end: '17:00' },
wednesday: { enabled: true, start: '09:00', end: '17:00' },
thursday: { enabled: true, start: '09:00', end: '17:00' },
friday: { enabled: true, start: '09:00', end: '17:00' },
saturday: { enabled: false },
sunday: { enabled: false },
},
}
};
if (!targetMCPTool) {
return {
instructions: Property.MarkDown({
value: `**Select an MCP tool above to configure queue settings.**`
}),
delayType: Property.StaticDropdown({
displayName: 'Delay Type',
description: 'Select an MCP tool first',
required: true,
defaultValue: 'fixed',
options: { options: [{ label: 'Select Tool First', value: 'fixed' }] },
}),
delayUnit: Property.StaticDropdown({
displayName: 'Delay Unit',
description: 'Select an MCP tool first',
required: true,
defaultValue: 'seconds',
options: { options: [{ label: 'Select Tool First', value: 'seconds' }] },
}),
delayValue: Property.Number({
displayName: 'Delay Value',
description: 'Select an MCP tool first',
required: false,
defaultValue: 0,
}),
delayMin: Property.Number({
displayName: 'Minimum Delay',
description: 'Select an MCP tool first',
required: false,
defaultValue: 0,
}),
delayMax: Property.Number({
displayName: 'Maximum Delay',
description: 'Select an MCP tool first',
required: false,
defaultValue: 0,
}),
dailyLimit: Property.Number({
displayName: 'Daily Limit',
description: 'Select an MCP tool first',
required: false,
defaultValue: 0,
}),
hourlyLimit: Property.Number({
displayName: 'Hourly Limit',
description: 'Select an MCP tool first',
required: false,
defaultValue: 0,
}),
activeHours: Property.Object({
displayName: 'Active Hours',
description: 'Select an MCP tool first',
required: false,
defaultValue: {},
}),
};
}
const authConfig = auth as { mcpServerUrl?: string };
if (!authConfig?.mcpServerUrl) {
return {
instructions: Property.MarkDown({
value: `**Error:** MCP Server URL required in auth configuration.`
}),
delayType: Property.StaticDropdown({
displayName: 'Delay Type',
description: 'Configure auth first',
required: true,
defaultValue: 'fixed',
options: { options: [{ label: 'Configure Auth First', value: 'fixed' }] },
}),
delayUnit: Property.StaticDropdown({
displayName: 'Delay Unit',
description: 'Configure auth first',
required: true,
defaultValue: 'seconds',
options: { options: [{ label: 'Configure Auth First', value: 'seconds' }] },
}),
delayValue: Property.Number({
displayName: 'Delay Value',
description: 'Configure auth first',
required: false,
defaultValue: 0,
}),
delayMin: Property.Number({
displayName: 'Minimum Delay',
description: 'Configure auth first',
required: false,
defaultValue: 0,
}),
delayMax: Property.Number({
displayName: 'Maximum Delay',
description: 'Configure auth first',
required: false,
defaultValue: 0,
}),
dailyLimit: Property.Number({
displayName: 'Daily Limit',
description: 'Configure auth first',
required: false,
defaultValue: 0,
}),
hourlyLimit: Property.Number({
displayName: 'Hourly Limit',
description: 'Configure auth first',
required: false,
defaultValue: 0,
}),
activeHours: Property.Object({
displayName: 'Active Hours',
description: 'Configure auth first',
required: false,
defaultValue: {},
}),
};
}
// PropertyContext doesn't have store access - context.store is undefined
const queueId = `mcp_${targetMCPTool}_queue`;
return {
instructions: Property.MarkDown({
value: `**⚙️ Configure queue:** ${queueId}\n\n*Note: Form shows defaults due to ActivePieces PropertyContext limitations. If this queue exists, your current settings will be preserved when you submit.*`
}),
delayType: Property.StaticDropdown({
displayName: 'Delay Type',
description: 'Type of delay between queue items',
required: true,
defaultValue: defaults.delayType,
options: {
options: [
{ label: 'Fixed Delay', value: 'fixed' },
{ label: 'Random Delay', value: 'random' },
],
},
}),
delayUnit: Property.StaticDropdown({
displayName: 'Delay Unit',
description: 'Time unit for delays',
required: true,
defaultValue: defaults.delayUnit,
options: {
options: [
{ label: 'Seconds', value: 'seconds' },
{ label: 'Minutes', value: 'minutes' },
{ label: 'Hours', value: 'hours' },
{ label: 'Days', value: 'days' },
],
},
}),
delayValue: Property.Number({
displayName: 'Delay Value',
description: 'Fixed delay amount between items',
required: false,
defaultValue: defaults.delayValue,
}),
delayMin: Property.Number({
displayName: 'Minimum Delay',
description: 'Minimum delay for random delays',
required: false,
defaultValue: defaults.delayMin,
}),
delayMax: Property.Number({
displayName: 'Maximum Delay',
description: 'Maximum delay for random delays',
required: false,
defaultValue: defaults.delayMax,
}),
dailyLimit: Property.Number({
displayName: 'Daily Limit',
description: 'Maximum items to process per day (0 = unlimited)',
required: false,
defaultValue: defaults.dailyLimit,
}),
hourlyLimit: Property.Number({
displayName: 'Hourly Limit',
description: 'Maximum items to process per hour (0 = unlimited)',
required: false,
defaultValue: defaults.hourlyLimit,
}),
activeHours: Property.Object({
displayName: 'Active Hours',
description: 'Only process queue during these hours (leave empty for 24×7)',
required: false,
defaultValue: defaults.activeHours,
}),
};
},
}),
},
async run(context: ActionContext) {
try {
const {
targetMCPTool,
delayType,
delayUnit,
delayValue,
delayMin,
delayMax,
dailyLimit,
hourlyLimit,
activeHours,
} = context.propsValue;
const authConfig = context.auth as { mcpServerUrl?: string };
const mcpServerUrl = authConfig.mcpServerUrl;
/* -------- Validation -------- */
// Basic required fields
if (!mcpServerUrl) {
throw new Error('MCP Server URL is required in auth configuration');
}
if (!targetMCPTool) {
throw new Error('MCP Tool selection is required');
}
// Delay validation
if (delayType === 'fixed') {
if (!delayValue || delayValue <= 0) {
throw new Error(`Delay value must be > 0 for fixed delays. Got: ${delayValue}`);
}
} else if (delayType === 'random') {
if (!delayMin || !delayMax || delayMin <= 0 || delayMax <= 0) {
throw new Error(`Min/Max delays must be > 0 for random delays. Got min: ${delayMin}, max: ${delayMax}`);
}
if (delayMin >= delayMax) {
throw new Error(`Minimum delay (${delayMin}) must be less than maximum delay (${delayMax})`);
}
}
// Limits validation
if (dailyLimit && dailyLimit < 0) {
throw new Error(`Daily limit must be >= 0. Got: ${dailyLimit}`);
}
if (hourlyLimit && hourlyLimit < 0) {
throw new Error(`Hourly limit must be >= 0. Got: ${hourlyLimit}`);
}
// Active hours validation
if (activeHours) {
const value = activeHours as any;
if (!value || typeof value !== 'object') {
throw new Error('Active hours must be an object');
}
if (!value.timezone || typeof value.timezone !== 'string' || !value.timezone.includes('/')) {
throw new Error(`timezone must be a valid IANA name, e.g., America/Toronto. Got: ${value.timezone}`);
}
// Handle schedule as either JSON string or object
let schedule = value.schedule;
if (typeof schedule === 'string') {
try {
schedule = JSON.parse(schedule);
} catch (parseError: any) {
throw new Error(`Schedule JSON string is invalid: ${parseError.message}`);
}
}
if (!schedule || typeof schedule !== 'object') {
throw new Error(`Active hours schedule must be an object or valid JSON string. Got type: ${typeof value.schedule}`);
}
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d$/; // More precise regex: 00:00 to 23:59
for (const d of days) {
const entry = schedule[d];
if (!entry) {
throw new Error(`Missing schedule for ${d}`);
}
if (typeof entry.enabled !== 'boolean') {
throw new Error(`${d}.enabled must be boolean. Got: ${typeof entry.enabled} (${entry.enabled})`);
}
if (entry.enabled) {
if (!entry.start || !timeRegex.test(entry.start)) {
throw new Error(`${d} start time must be HH:MM format (00:00-23:59). Got: "${entry.start}"`);
}
if (!entry.end || !timeRegex.test(entry.end)) {
throw new Error(`${d} end time must be HH:MM format (00:00-23:59). Got: "${entry.end}"`);
}
// Convert to minutes for proper comparison
const startMinutes = parseInt(entry.start.split(':')[0]) * 60 + parseInt(entry.start.split(':')[1]);
const endMinutes = parseInt(entry.end.split(':')[0]) * 60 + parseInt(entry.end.split(':')[1]);
if (startMinutes >= endMinutes) {
throw new Error(`${d} start time (${entry.start}) must be before end time (${entry.end})`);
}
}
}
// Update the activeHours with parsed schedule for storage
if (typeof value.schedule === 'string') {
value.schedule = schedule;
}
}
/* -------- Build / Update configuration -------- */
const queueId = `mcp_${targetMCPTool}_queue`;
// Try to get existing configuration
let existing: QueueConfiguration | null = null;
try {
existing = await QueueManager.getQueueConfiguration(context, queueId);
} catch (error) {
// Queue doesn't exist (normal for new queues)
existing = null;
}
const now = Date.now();
const queueConfig: QueueConfiguration = {
id: queueId,
mcpToolName: targetMCPTool as string,
delayType: delayType as any,
delayUnit: delayUnit as any,
delayValue,
delayMin,
delayMax,
dailyLimit: dailyLimit || 0,
hourlyLimit: hourlyLimit || 0,
activeHours: activeHours || undefined,
createdAt: existing?.createdAt || now,
updatedAt: now,
lastUsed: existing?.lastUsed,
totalProcessed: existing?.totalProcessed || 0,
};
// Save queue configuration
try {
await context.store.put(`queue_config_${queueId}`, queueConfig, StoreScope.PROJECT);
} catch (storeError: any) {
throw new Error(`Failed to save queue configuration: ${storeError.message}`);
}
// Create initial queue state if this is a new queue
if (!existing) {
const initState: QueueState = {
queueId,
lastReleaseTime: 0,
itemCount: 0,
currentExecutingItem: null,
lastExecutedTime: 0,
version: 0,
};
try {
await context.store.put(`queue_state_${queueId}`, initState, StoreScope.PROJECT);
} catch (storeError: any) {
throw new Error(`Failed to create initial queue state: ${storeError.message}`);
}
}
// Get queue status
let status;
try {
status = await QueueManager.getQueueStatus(context, queueId);
} catch (statusError: any) {
throw new Error(`Failed to get queue status: ${statusError.message}`);
}
return {
success: true,
queueId,
queueLabel: `MCP: ${targetMCPTool}`,
toolName: targetMCPTool,
created: !existing,
updated: !!existing,
status,
};
} catch (error: any) {
throw new Error(`Queue creation failed: ${error.message}`);
}
},
});