Add Claude N8N toolkit with Docker mock API server
- Added comprehensive N8N development tools collection - Added Docker-containerized mock API server for testing - Added complete documentation and setup guides - Added mock API server with health checks and data endpoints - Tools include workflow analyzers, debuggers, and controllers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
456
claude_n8n/tools/manual_trigger_manager.py
Normal file
456
claude_n8n/tools/manual_trigger_manager.py
Normal file
@@ -0,0 +1,456 @@
|
||||
"""
|
||||
Manual Trigger Manager for N8N Workflows
|
||||
|
||||
This module provides functionality to manage manual triggers for N8N workflows,
|
||||
including webhook-based triggers and manual execution capabilities.
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .n8n_client import N8NClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ManualTriggerManager:
|
||||
"""Manager for creating and handling manual triggers in N8N workflows."""
|
||||
|
||||
def __init__(self, client: Optional[N8NClient] = None):
|
||||
"""
|
||||
Initialize the manual trigger manager.
|
||||
|
||||
Args:
|
||||
client: N8N client instance. If None, creates a new one.
|
||||
"""
|
||||
self.client = client or N8NClient()
|
||||
|
||||
def add_manual_trigger_to_workflow(self, workflow_id: str,
|
||||
trigger_type: str = "manual") -> Dict[str, Any]:
|
||||
"""
|
||||
Add a manual trigger to an existing workflow.
|
||||
|
||||
Args:
|
||||
workflow_id: ID of the workflow to modify
|
||||
trigger_type: Type of trigger ('manual', 'webhook', 'http')
|
||||
|
||||
Returns:
|
||||
Result of the operation including trigger details
|
||||
"""
|
||||
try:
|
||||
workflow = self.client.get_workflow(workflow_id)
|
||||
workflow_name = workflow.get('name', 'Unknown')
|
||||
|
||||
# Check if workflow already has the specified trigger type
|
||||
nodes = workflow.get('nodes', [])
|
||||
existing_trigger = self._find_trigger_node(nodes, trigger_type)
|
||||
|
||||
if existing_trigger:
|
||||
return {
|
||||
'success': True,
|
||||
'message': f"Workflow {workflow_name} already has a {trigger_type} trigger",
|
||||
'trigger_node': existing_trigger,
|
||||
'added_new_trigger': False
|
||||
}
|
||||
|
||||
# Create the appropriate trigger node
|
||||
trigger_node = self._create_trigger_node(trigger_type, workflow_id)
|
||||
|
||||
# Add trigger node to workflow
|
||||
updated_nodes = [trigger_node] + nodes
|
||||
|
||||
# Update connections to connect trigger to first existing node
|
||||
connections = workflow.get('connections', {})
|
||||
if nodes and not self._has_trigger_connections(connections):
|
||||
first_node_name = nodes[0].get('name')
|
||||
if first_node_name:
|
||||
trigger_name = trigger_node['name']
|
||||
if trigger_name not in connections:
|
||||
connections[trigger_name] = {}
|
||||
connections[trigger_name]['main'] = [[{
|
||||
'node': first_node_name,
|
||||
'type': 'main',
|
||||
'index': 0
|
||||
}]]
|
||||
|
||||
# Update workflow
|
||||
updated_workflow = {
|
||||
**workflow,
|
||||
'nodes': updated_nodes,
|
||||
'connections': connections
|
||||
}
|
||||
|
||||
result = self.client.update_workflow(workflow_id, updated_workflow)
|
||||
|
||||
logger.info(f"Added {trigger_type} trigger to workflow: {workflow_name} ({workflow_id})")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f"Successfully added {trigger_type} trigger to {workflow_name}",
|
||||
'trigger_node': trigger_node,
|
||||
'workflow': result,
|
||||
'added_new_trigger': True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to add {trigger_type} trigger to workflow {workflow_id}: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'workflow_id': workflow_id
|
||||
}
|
||||
|
||||
def create_webhook_trigger_workflow(self, workflow_name: str,
|
||||
webhook_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new workflow with a webhook trigger.
|
||||
|
||||
Args:
|
||||
workflow_name: Name for the new workflow
|
||||
webhook_path: Custom webhook path (if None, generates random)
|
||||
|
||||
Returns:
|
||||
Created workflow with webhook details
|
||||
"""
|
||||
try:
|
||||
if not webhook_path:
|
||||
webhook_path = f"test-webhook-{str(uuid.uuid4())[:8]}"
|
||||
|
||||
# Create webhook trigger node
|
||||
webhook_node = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [100, 100],
|
||||
"webhookId": str(uuid.uuid4()),
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": webhook_path,
|
||||
"responseMode": "responseNode",
|
||||
"options": {
|
||||
"noResponseBody": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create a simple response node
|
||||
response_node = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": "Response",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [300, 100],
|
||||
"parameters": {
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Create workflow structure
|
||||
workflow_data = {
|
||||
"name": workflow_name,
|
||||
"active": False, # Start inactive for testing
|
||||
"nodes": [webhook_node, response_node],
|
||||
"connections": {
|
||||
"Webhook Trigger": {
|
||||
"main": [[{
|
||||
"node": "Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"timezone": "UTC"
|
||||
}
|
||||
}
|
||||
|
||||
# Create the workflow
|
||||
created_workflow = self.client.create_workflow(workflow_data)
|
||||
|
||||
# Get N8N base URL for webhook URL construction
|
||||
webhook_url = f"{self.client.base_url.replace('/api/v1', '')}/webhook-test/{webhook_path}"
|
||||
|
||||
logger.info(f"Created webhook trigger workflow: {workflow_name}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'workflow': created_workflow,
|
||||
'webhook_url': webhook_url,
|
||||
'webhook_path': webhook_path,
|
||||
'test_command': f"curl -X POST {webhook_url} -H 'Content-Type: application/json' -d '{{}}'",
|
||||
'message': f"Created workflow {workflow_name} with webhook trigger"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to create webhook trigger workflow: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg
|
||||
}
|
||||
|
||||
def execute_workflow_manually(self, workflow_id: str,
|
||||
input_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a workflow manually with optional input data.
|
||||
|
||||
Args:
|
||||
workflow_id: ID of the workflow to execute
|
||||
input_data: Optional input data for the execution
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
try:
|
||||
# Execute workflow
|
||||
if input_data:
|
||||
execution = self.client.execute_workflow(workflow_id, input_data)
|
||||
else:
|
||||
execution = self.client.execute_workflow(workflow_id)
|
||||
|
||||
logger.info(f"Manually executed workflow: {workflow_id}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'execution': execution,
|
||||
'execution_id': execution.get('id'),
|
||||
'message': f"Successfully executed workflow {workflow_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to execute workflow {workflow_id} manually: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'workflow_id': workflow_id
|
||||
}
|
||||
|
||||
def trigger_webhook_workflow(self, webhook_url: str,
|
||||
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Trigger a webhook-enabled workflow.
|
||||
|
||||
Args:
|
||||
webhook_url: URL of the webhook
|
||||
data: Data to send to webhook
|
||||
|
||||
Returns:
|
||||
Webhook response
|
||||
"""
|
||||
import requests
|
||||
|
||||
try:
|
||||
if data is None:
|
||||
data = {"test": True, "timestamp": "2024-01-01T00:00:00Z"}
|
||||
|
||||
response = requests.post(
|
||||
webhook_url,
|
||||
json=data,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
logger.info(f"Triggered webhook: {webhook_url}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'status_code': response.status_code,
|
||||
'response_data': response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text,
|
||||
'webhook_url': webhook_url,
|
||||
'sent_data': data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to trigger webhook {webhook_url}: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'webhook_url': webhook_url
|
||||
}
|
||||
|
||||
def setup_test_workflow(self, workflow_id: str,
|
||||
test_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Set up a workflow for testing by ensuring it has manual trigger and is inactive.
|
||||
|
||||
Args:
|
||||
workflow_id: ID of the workflow to set up
|
||||
test_data: Optional test data to associate with the workflow
|
||||
|
||||
Returns:
|
||||
Setup result with testing instructions
|
||||
"""
|
||||
try:
|
||||
workflow = self.client.get_workflow(workflow_id)
|
||||
workflow_name = workflow.get('name', 'Unknown')
|
||||
|
||||
# Ensure workflow is inactive
|
||||
if workflow.get('active', False):
|
||||
updated_workflow = {**workflow, 'active': False}
|
||||
workflow = self.client.update_workflow(workflow_id, updated_workflow)
|
||||
logger.info(f"Set workflow {workflow_name} to inactive for testing")
|
||||
|
||||
# Add manual trigger if not present
|
||||
trigger_result = self.add_manual_trigger_to_workflow(workflow_id, 'manual')
|
||||
|
||||
# Save test data if provided
|
||||
test_info = {}
|
||||
if test_data:
|
||||
from .mock_data_generator import MockDataGenerator
|
||||
data_gen = MockDataGenerator()
|
||||
test_file = data_gen.save_mock_data([test_data], f"test_data_{workflow_id}")
|
||||
test_info['test_data_file'] = test_file
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'workflow_name': workflow_name,
|
||||
'workflow_id': workflow_id,
|
||||
'is_inactive': True,
|
||||
'has_manual_trigger': True,
|
||||
'trigger_setup': trigger_result,
|
||||
'test_info': test_info,
|
||||
'testing_instructions': {
|
||||
'manual_execution': f"Use execute_workflow_manually('{workflow_id}') to test",
|
||||
'monitor_logs': "Use DockerLogMonitor to watch execution logs",
|
||||
'check_results': f"Use client.get_executions(workflow_id='{workflow_id}') to see results"
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to set up workflow for testing {workflow_id}: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'workflow_id': workflow_id
|
||||
}
|
||||
|
||||
def _find_trigger_node(self, nodes: List[Dict[str, Any]],
|
||||
trigger_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Find a trigger node of specific type in the nodes list."""
|
||||
trigger_types = {
|
||||
'manual': 'n8n-nodes-base.manualTrigger',
|
||||
'webhook': 'n8n-nodes-base.webhook',
|
||||
'http': 'n8n-nodes-base.httpRequest'
|
||||
}
|
||||
|
||||
target_type = trigger_types.get(trigger_type)
|
||||
if not target_type:
|
||||
return None
|
||||
|
||||
for node in nodes:
|
||||
if node.get('type') == target_type:
|
||||
return node
|
||||
|
||||
return None
|
||||
|
||||
def _create_trigger_node(self, trigger_type: str, workflow_id: str) -> Dict[str, Any]:
|
||||
"""Create a trigger node of the specified type."""
|
||||
node_id = str(uuid.uuid4())
|
||||
|
||||
if trigger_type == 'manual':
|
||||
return {
|
||||
"id": node_id,
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 100],
|
||||
"parameters": {}
|
||||
}
|
||||
|
||||
elif trigger_type == 'webhook':
|
||||
webhook_path = f"test-{workflow_id[:8]}-{str(uuid.uuid4())[:8]}"
|
||||
return {
|
||||
"id": node_id,
|
||||
"name": "Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [50, 100],
|
||||
"webhookId": str(uuid.uuid4()),
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": webhook_path,
|
||||
"responseMode": "responseNode",
|
||||
"options": {
|
||||
"noResponseBody": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elif trigger_type == 'http':
|
||||
return {
|
||||
"id": node_id,
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4,
|
||||
"position": [50, 100],
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://httpbin.org/json",
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported trigger type: {trigger_type}")
|
||||
|
||||
def _has_trigger_connections(self, connections: Dict[str, Any]) -> bool:
|
||||
"""Check if connections already include trigger nodes."""
|
||||
trigger_names = ['Manual Trigger', 'Webhook Trigger', 'HTTP Request']
|
||||
return any(name in connections for name in trigger_names)
|
||||
|
||||
def get_workflow_triggers(self, workflow_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all trigger nodes in a workflow.
|
||||
|
||||
Args:
|
||||
workflow_id: ID of the workflow
|
||||
|
||||
Returns:
|
||||
List of trigger nodes
|
||||
"""
|
||||
try:
|
||||
workflow = self.client.get_workflow(workflow_id)
|
||||
nodes = workflow.get('nodes', [])
|
||||
|
||||
trigger_types = [
|
||||
'n8n-nodes-base.manualTrigger',
|
||||
'n8n-nodes-base.webhook',
|
||||
'n8n-nodes-base.httpRequest',
|
||||
'n8n-nodes-base.cron',
|
||||
'n8n-nodes-base.interval'
|
||||
]
|
||||
|
||||
triggers = []
|
||||
for node in nodes:
|
||||
if node.get('type') in trigger_types:
|
||||
trigger_info = {
|
||||
'id': node.get('id'),
|
||||
'name': node.get('name'),
|
||||
'type': node.get('type'),
|
||||
'parameters': node.get('parameters', {}),
|
||||
'position': node.get('position', [0, 0])
|
||||
}
|
||||
|
||||
# Add webhook-specific info
|
||||
if node.get('type') == 'n8n-nodes-base.webhook':
|
||||
webhook_path = node.get('parameters', {}).get('path', '')
|
||||
webhook_url = f"{self.client.base_url.replace('/api/v1', '')}/webhook-test/{webhook_path}"
|
||||
trigger_info['webhook_url'] = webhook_url
|
||||
trigger_info['webhook_path'] = webhook_path
|
||||
|
||||
triggers.append(trigger_info)
|
||||
|
||||
return triggers
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get workflow triggers for {workflow_id}: {e}")
|
||||
return []
|
||||
|
||||
def create_manual_trigger_manager():
|
||||
"""Create a manual trigger manager instance."""
|
||||
return ManualTriggerManager()
|
||||
Reference in New Issue
Block a user