Initial commit: MCP Hub Community Edition v3.0.0

Community edition generated from private repo via sync pipeline.
Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n,
Supabase, OpenPanel, Appwrite, Directus) with ~587 tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

6
plugins/n8n/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""n8n Automation Plugin - Workflow Management"""
from plugins.n8n.client import N8nClient
from plugins.n8n.plugin import N8nPlugin
__all__ = ["N8nPlugin", "N8nClient"]

401
plugins/n8n/client.py Normal file
View File

@@ -0,0 +1,401 @@
"""
n8n REST API Client
Handles all HTTP communication with n8n REST API.
Separates API communication from business logic.
"""
import logging
from typing import Any
import aiohttp
class N8nClient:
"""
n8n REST API client for HTTP communication.
Handles authentication, request formatting, and error handling
for all n8n API endpoints.
Authentication: API Key via X-N8N-API-KEY header
"""
def __init__(self, site_url: str, api_key: str):
"""
Initialize n8n API client.
Args:
site_url: n8n instance URL (e.g., https://n8n.example.com)
api_key: n8n API key for authentication
"""
self.site_url = site_url.rstrip("/")
self.api_base = f"{self.site_url}/api/v1"
self.api_key = api_key
# Initialize logger
self.logger = logging.getLogger(f"N8nClient.{site_url}")
def _get_headers(self, additional_headers: dict | None = None) -> dict[str, str]:
"""
Get request headers with API key authentication.
Args:
additional_headers: Additional headers to include
Returns:
Dict: Headers with authentication
"""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-N8N-API-KEY": self.api_key,
}
# Merge additional headers
if additional_headers:
headers.update(additional_headers)
return headers
async def request(
self,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | None = None,
headers_override: dict | None = None,
) -> Any:
"""
Make authenticated request to n8n REST API.
Args:
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
endpoint: API endpoint (without base URL)
params: Query parameters
json_data: JSON body data
headers_override: Override default headers
Returns:
API response (dict, list, or None)
Raises:
Exception: On API errors with status code and message
"""
# Build full URL
url = f"{self.api_base}/{endpoint.lstrip('/')}"
# Setup headers
headers = self._get_headers(headers_override)
# Filter out None values from params
if params:
params = {k: v for k, v in params.items() if v is not None}
# Filter None values from JSON data
if json_data:
json_data = {k: v for k, v in json_data.items() if v is not None}
# Make request
self.logger.debug(f"{method} {url}")
self.logger.debug(f"Params: {params}")
self.logger.debug(f"Data: {json_data}")
async with (
aiohttp.ClientSession() as session,
session.request(
method=method, url=url, params=params, json=json_data, headers=headers
) as response,
):
# Log response
self.logger.debug(f"Response status: {response.status}")
# Handle empty responses (e.g., 204 No Content)
if response.status == 204:
return {"success": True, "message": "Operation completed successfully"}
# Try to parse JSON response
try:
response_data = await response.json()
except Exception:
response_text = await response.text()
if response.status >= 400:
raise Exception(f"n8n API error (status {response.status}): {response_text}")
return {"success": True, "message": response_text}
# Check for errors
if response.status >= 400:
error_msg = response_data.get("message", str(response_data))
raise Exception(f"n8n API error (status {response.status}): {error_msg}")
return response_data
# =====================
# WORKFLOW ENDPOINTS
# =====================
async def list_workflows(
self,
active: bool | None = None,
tags: str | None = None,
name: str | None = None,
limit: int = 50,
cursor: str | None = None,
) -> dict[str, Any]:
"""List workflows with optional filters"""
params = {"active": active, "tags": tags, "name": name, "limit": limit, "cursor": cursor}
return await self.request("GET", "workflows", params=params)
async def get_workflow(self, workflow_id: str) -> dict[str, Any]:
"""Get workflow by ID"""
return await self.request("GET", f"workflows/{workflow_id}")
async def create_workflow(self, data: dict[str, Any]) -> dict[str, Any]:
"""Create a new workflow"""
return await self.request("POST", "workflows", json_data=data)
async def update_workflow(self, workflow_id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update an existing workflow"""
return await self.request("PUT", f"workflows/{workflow_id}", json_data=data)
async def delete_workflow(self, workflow_id: str) -> dict[str, Any]:
"""Delete a workflow"""
return await self.request("DELETE", f"workflows/{workflow_id}")
async def activate_workflow(self, workflow_id: str) -> dict[str, Any]:
"""Activate a workflow"""
return await self.request("POST", f"workflows/{workflow_id}/activate")
async def deactivate_workflow(self, workflow_id: str) -> dict[str, Any]:
"""Deactivate a workflow"""
return await self.request("POST", f"workflows/{workflow_id}/deactivate")
async def execute_workflow(
self, workflow_id: str, data: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Execute a workflow manually"""
return await self.request("POST", f"workflows/{workflow_id}/run", json_data=data or {})
async def get_workflow_tags(self, workflow_id: str) -> list[dict[str, Any]]:
"""Get tags assigned to a workflow"""
workflow = await self.get_workflow(workflow_id)
return workflow.get("tags", [])
# =====================
# EXECUTION ENDPOINTS
# =====================
async def list_executions(
self,
workflow_id: str | None = None,
status: str | None = None,
include_data: bool = False,
limit: int = 20,
cursor: str | None = None,
) -> dict[str, Any]:
"""List workflow executions with filters"""
params = {
"workflowId": workflow_id,
"status": status,
"includeData": str(include_data).lower(),
"limit": limit,
"cursor": cursor,
}
return await self.request("GET", "executions", params=params)
async def get_execution(self, execution_id: str, include_data: bool = True) -> dict[str, Any]:
"""Get execution details"""
params = {"includeData": str(include_data).lower()}
return await self.request("GET", f"executions/{execution_id}", params=params)
async def delete_execution(self, execution_id: str) -> dict[str, Any]:
"""Delete a single execution"""
return await self.request("DELETE", f"executions/{execution_id}")
async def stop_execution(self, execution_id: str) -> dict[str, Any]:
"""Stop a running execution"""
return await self.request("POST", f"executions/{execution_id}/stop")
# =====================
# CREDENTIAL ENDPOINTS
# =====================
async def list_credentials(self, limit: int = 100, cursor: str | None = None) -> dict[str, Any]:
"""List all credentials (metadata only)"""
params = {"limit": limit, "cursor": cursor}
return await self.request("GET", "credentials", params=params)
async def get_credential(self, credential_id: str) -> dict[str, Any]:
"""Get credential metadata"""
return await self.request("GET", f"credentials/{credential_id}")
async def create_credential(self, data: dict[str, Any]) -> dict[str, Any]:
"""Create a new credential"""
return await self.request("POST", "credentials", json_data=data)
async def delete_credential(self, credential_id: str) -> dict[str, Any]:
"""Delete a credential"""
return await self.request("DELETE", f"credentials/{credential_id}")
async def get_credential_schema(self, credential_type: str) -> dict[str, Any]:
"""Get schema for a credential type"""
return await self.request("GET", f"credentials/schema/{credential_type}")
async def transfer_credential(
self, credential_id: str, destination_project_id: str
) -> dict[str, Any]:
"""Transfer credential to another project"""
data = {"destinationProjectId": destination_project_id}
return await self.request("POST", f"credentials/{credential_id}/transfer", json_data=data)
# =====================
# TAG ENDPOINTS
# =====================
async def list_tags(self, limit: int = 100, cursor: str | None = None) -> dict[str, Any]:
"""List all tags"""
params = {"limit": limit, "cursor": cursor}
return await self.request("GET", "tags", params=params)
async def get_tag(self, tag_id: str) -> dict[str, Any]:
"""Get tag by ID"""
return await self.request("GET", f"tags/{tag_id}")
async def create_tag(self, name: str) -> dict[str, Any]:
"""Create a new tag"""
return await self.request("POST", "tags", json_data={"name": name})
async def update_tag(self, tag_id: str, name: str) -> dict[str, Any]:
"""Update tag name"""
return await self.request("PUT", f"tags/{tag_id}", json_data={"name": name})
async def delete_tag(self, tag_id: str) -> dict[str, Any]:
"""Delete a tag"""
return await self.request("DELETE", f"tags/{tag_id}")
# =====================
# USER ENDPOINTS
# =====================
async def list_users(
self, limit: int = 100, cursor: str | None = None, include_role: bool = True
) -> dict[str, Any]:
"""List all users"""
params = {"limit": limit, "cursor": cursor, "includeRole": str(include_role).lower()}
return await self.request("GET", "users", params=params)
async def get_user(self, user_id: str, include_role: bool = True) -> dict[str, Any]:
"""Get user by ID or email"""
params = {"includeRole": str(include_role).lower()}
return await self.request("GET", f"users/{user_id}", params=params)
async def create_user(self, users: list[dict[str, Any]]) -> dict[str, Any]:
"""Create/invite users"""
return await self.request("POST", "users", json_data=users)
async def delete_user(self, user_id: str) -> dict[str, Any]:
"""Delete a user"""
return await self.request("DELETE", f"users/{user_id}")
async def change_user_role(self, user_id: str, new_role: str) -> dict[str, Any]:
"""Change user's global role"""
return await self.request(
"PATCH", f"users/{user_id}/role", json_data={"newRoleName": new_role}
)
# =====================
# PROJECT ENDPOINTS (Enterprise/Pro)
# =====================
async def list_projects(self, limit: int = 100, cursor: str | None = None) -> dict[str, Any]:
"""List all projects"""
params = {"limit": limit, "cursor": cursor}
return await self.request("GET", "projects", params=params)
async def get_project(self, project_id: str) -> dict[str, Any]:
"""Get project by ID"""
return await self.request("GET", f"projects/{project_id}")
async def create_project(self, name: str) -> dict[str, Any]:
"""Create a new project"""
return await self.request("POST", "projects", json_data={"name": name})
async def update_project(self, project_id: str, name: str) -> dict[str, Any]:
"""Update project"""
return await self.request("PUT", f"projects/{project_id}", json_data={"name": name})
async def delete_project(self, project_id: str) -> dict[str, Any]:
"""Delete a project"""
return await self.request("DELETE", f"projects/{project_id}")
async def add_project_users(
self, project_id: str, relations: list[dict[str, str]]
) -> dict[str, Any]:
"""Add users to project with roles"""
return await self.request("POST", f"projects/{project_id}/users", json_data=relations)
async def change_project_user_role(
self, project_id: str, user_id: str, role: str
) -> dict[str, Any]:
"""Change user's role in project"""
return await self.request(
"PUT", f"projects/{project_id}/users/{user_id}", json_data={"role": role}
)
async def remove_project_user(self, project_id: str, user_id: str) -> dict[str, Any]:
"""Remove user from project"""
return await self.request("DELETE", f"projects/{project_id}/users/{user_id}")
# =====================
# VARIABLE ENDPOINTS
# =====================
async def list_variables(self, limit: int = 100, cursor: str | None = None) -> dict[str, Any]:
"""List all variables"""
params = {"limit": limit, "cursor": cursor}
return await self.request("GET", "variables", params=params)
async def get_variable(self, key: str) -> dict[str, Any]:
"""Get variable by key"""
return await self.request("GET", f"variables/{key}")
async def create_variable(self, key: str, value: str) -> dict[str, Any]:
"""Create a new variable"""
return await self.request("POST", "variables", json_data={"key": key, "value": value})
async def update_variable(self, key: str, value: str) -> dict[str, Any]:
"""Update variable value"""
return await self.request("PUT", f"variables/{key}", json_data={"value": value})
async def delete_variable(self, key: str) -> dict[str, Any]:
"""Delete a variable"""
return await self.request("DELETE", f"variables/{key}")
# =====================
# SYSTEM ENDPOINTS
# =====================
async def run_audit(self, categories: list[str] | None = None) -> dict[str, Any]:
"""Run security audit"""
data = {}
if categories:
data["additionalOptions"] = {"categories": categories}
return await self.request("POST", "audit", json_data=data)
async def source_control_pull(
self, variables: dict[str, str] | None = None, force: bool = False
) -> dict[str, Any]:
"""Pull from source control"""
data = {"force": force}
if variables:
data["variables"] = variables
return await self.request("POST", "source-control/pull", json_data=data)
async def health_check(self) -> dict[str, Any]:
"""Check n8n instance health"""
# Use direct URL, not API base
url = f"{self.site_url}/healthz"
async with aiohttp.ClientSession() as session, session.get(url) as response:
if response.status == 200:
return {"healthy": True, "status": "ok"}
else:
return {"healthy": False, "status": f"unhealthy (status {response.status})"}

View File

@@ -0,0 +1,23 @@
"""n8n Plugin Handlers"""
from plugins.n8n.handlers import (
credentials,
executions,
projects,
system,
tags,
users,
variables,
workflows,
)
__all__ = [
"workflows",
"executions",
"credentials",
"tags",
"users",
"projects",
"variables",
"system",
]

View File

@@ -0,0 +1,161 @@
"""Credentials Handler - manages n8n credentials"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "get_credential",
"method_name": "get_credential",
"description": "Get credential metadata by ID. Note: Listing all credentials is not supported in n8n Public API.",
"schema": {
"type": "object",
"properties": {"credential_id": {"type": "string", "minLength": 1}},
"required": ["credential_id"],
},
"scope": "read",
},
{
"name": "create_credential",
"method_name": "create_credential",
"description": "Create a new credential. Use get_credential_schema to see required fields.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "description": "Credential name"},
"type": {
"type": "string",
"minLength": 1,
"description": "Credential type (e.g., 'githubApi')",
},
"data": {
"type": "object",
"description": "Credential data matching the type schema",
},
},
"required": ["name", "type", "data"],
},
"scope": "admin",
},
{
"name": "delete_credential",
"method_name": "delete_credential",
"description": "Delete a credential.",
"schema": {
"type": "object",
"properties": {"credential_id": {"type": "string", "minLength": 1}},
"required": ["credential_id"],
},
"scope": "admin",
},
{
"name": "get_credential_schema",
"method_name": "get_credential_schema",
"description": "Get the schema for a credential type. Shows required fields.",
"schema": {
"type": "object",
"properties": {
"credential_type": {
"type": "string",
"minLength": 1,
"description": "Credential type name",
}
},
"required": ["credential_type"],
},
"scope": "read",
},
{
"name": "transfer_credential",
"method_name": "transfer_credential",
"description": "[Enterprise] Transfer a credential to another project. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"credential_id": {"type": "string", "minLength": 1},
"destination_project_id": {"type": "string", "minLength": 1},
},
"required": ["credential_id", "destination_project_id"],
},
"scope": "admin",
},
]
# === HANDLER FUNCTIONS ===
# Note: list_credentials is not supported in n8n Public API (GET /credentials returns 405)
async def get_credential(client: N8nClient, credential_id: str) -> str:
"""Get credential metadata"""
try:
cred = await client.get_credential(credential_id)
result = {
"success": True,
"credential": {
"id": cred.get("id"),
"name": cred.get("name"),
"type": cred.get("type"),
"created_at": cred.get("createdAt"),
"updated_at": cred.get("updatedAt"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_credential(client: N8nClient, name: str, type: str, data: dict[str, Any]) -> str:
"""Create a new credential"""
try:
cred_data = {"name": name, "type": type, "data": data}
cred = await client.create_credential(cred_data)
result = {
"success": True,
"message": f"Credential '{name}' created successfully",
"credential": {
"id": cred.get("id"),
"name": cred.get("name"),
"type": cred.get("type"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_credential(client: N8nClient, credential_id: str) -> str:
"""Delete a credential"""
try:
await client.delete_credential(credential_id)
return json.dumps(
{"success": True, "message": f"Credential {credential_id} deleted"}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_credential_schema(client: N8nClient, credential_type: str) -> str:
"""Get credential type schema"""
try:
schema = await client.get_credential_schema(credential_type)
return json.dumps(
{"success": True, "credential_type": credential_type, "schema": schema}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def transfer_credential(
client: N8nClient, credential_id: str, destination_project_id: str
) -> str:
"""Transfer credential to another project"""
try:
await client.transfer_credential(credential_id, destination_project_id)
return json.dumps(
{
"success": True,
"message": f"Credential {credential_id} transferred to project {destination_project_id}",
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,437 @@
"""Execution Handler - manages n8n workflow executions"""
import asyncio
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === LIST EXECUTIONS ===
{
"name": "list_executions",
"method_name": "list_executions",
"description": "List workflow executions with filters by status, workflow, and date. Returns execution history. All filter parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "OPTIONAL: Filter by workflow ID. Omit for all executions.",
},
"status": {
"type": "string",
"enum": ["success", "error", "waiting", "running", "new"],
"description": "OPTIONAL: Filter by execution status. Omit for all statuses.",
},
"include_data": {
"type": "boolean",
"description": "Include full execution data",
"default": False,
},
"limit": {
"type": "integer",
"description": "Maximum results",
"default": 20,
"minimum": 1,
"maximum": 250,
},
"cursor": {"type": "string", "description": "OPTIONAL: Pagination cursor."},
},
},
"scope": "read",
},
# === GET EXECUTION ===
{
"name": "get_execution",
"method_name": "get_execution",
"description": "Get detailed information about a specific execution including status, timing, and node outputs.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID",
"minLength": 1,
},
"include_data": {
"type": "boolean",
"description": "Include full node execution data",
"default": True,
},
},
"required": ["execution_id"],
},
"scope": "read",
},
# === DELETE EXECUTION ===
{
"name": "delete_execution",
"method_name": "delete_execution",
"description": "Delete a single execution record. This removes the execution from history.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID to delete",
"minLength": 1,
}
},
"required": ["execution_id"],
},
"scope": "write",
},
# === DELETE EXECUTIONS (BULK) ===
{
"name": "delete_executions",
"method_name": "delete_executions",
"description": "Bulk delete multiple execution records.",
"schema": {
"type": "object",
"properties": {
"execution_ids": {
"type": "array",
"description": "List of execution IDs to delete",
"items": {"type": "string"},
"minItems": 1,
}
},
"required": ["execution_ids"],
},
"scope": "write",
},
# === STOP EXECUTION ===
{
"name": "stop_execution",
"method_name": "stop_execution",
"description": "Stop a currently running execution.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID to stop",
"minLength": 1,
}
},
"required": ["execution_id"],
},
"scope": "write",
},
# === RETRY EXECUTION ===
{
"name": "retry_execution",
"method_name": "retry_execution",
"description": "Retry a failed execution. Creates a new execution with the same input data.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID to retry",
"minLength": 1,
}
},
"required": ["execution_id"],
},
"scope": "write",
},
# === GET EXECUTION DATA ===
{
"name": "get_execution_data",
"method_name": "get_execution_data",
"description": "Get full execution data including all node inputs and outputs. Useful for debugging.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID",
"minLength": 1,
}
},
"required": ["execution_id"],
},
"scope": "read",
},
# === WAIT FOR EXECUTION ===
{
"name": "wait_for_execution",
"method_name": "wait_for_execution",
"description": "Poll an execution until it completes. Returns final status and output.",
"schema": {
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "Execution ID to wait for",
"minLength": 1,
},
"timeout_seconds": {
"type": "integer",
"description": "Maximum seconds to wait",
"default": 60,
"minimum": 5,
"maximum": 300,
},
"poll_interval": {
"type": "integer",
"description": "Seconds between status checks",
"default": 2,
"minimum": 1,
"maximum": 30,
},
},
"required": ["execution_id"],
},
"scope": "read",
},
]
# === HANDLER FUNCTIONS ===
async def list_executions(
client: N8nClient,
workflow_id: str | None = None,
status: str | None = None,
include_data: bool = False,
limit: int = 20,
cursor: str | None = None,
) -> str:
"""List workflow executions"""
try:
response = await client.list_executions(
workflow_id=workflow_id,
status=status,
include_data=include_data,
limit=limit,
cursor=cursor,
)
executions = response.get("data", [])
next_cursor = response.get("nextCursor")
result = {
"success": True,
"count": len(executions),
"executions": [
{
"id": e.get("id"),
"workflow_id": e.get("workflowId"),
"workflow_name": e.get("workflowData", {}).get("name"),
"status": e.get("status"),
"finished": e.get("finished"),
"started_at": e.get("startedAt"),
"stopped_at": e.get("stoppedAt"),
"mode": e.get("mode"),
}
for e in executions
],
"next_cursor": next_cursor,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_execution(client: N8nClient, execution_id: str, include_data: bool = True) -> str:
"""Get execution details"""
try:
execution = await client.get_execution(execution_id, include_data)
result = {
"success": True,
"execution": {
"id": execution.get("id"),
"workflow_id": execution.get("workflowId"),
"workflow_name": execution.get("workflowData", {}).get("name"),
"status": execution.get("status"),
"finished": execution.get("finished"),
"started_at": execution.get("startedAt"),
"stopped_at": execution.get("stoppedAt"),
"mode": execution.get("mode"),
"retry_of": execution.get("retryOf"),
"retry_success_id": execution.get("retrySuccessId"),
},
}
# Include node execution data if requested
if include_data and "data" in execution:
result["execution"]["node_data"] = execution.get("data")
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_execution(client: N8nClient, execution_id: str) -> str:
"""Delete a single execution"""
try:
await client.delete_execution(execution_id)
result = {"success": True, "message": f"Execution {execution_id} deleted successfully"}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_executions(client: N8nClient, execution_ids: list[str]) -> str:
"""Bulk delete executions"""
try:
deleted = []
failed = []
for exec_id in execution_ids:
try:
await client.delete_execution(exec_id)
deleted.append(exec_id)
except Exception as e:
failed.append({"id": exec_id, "error": str(e)})
result = {
"success": len(failed) == 0,
"deleted_count": len(deleted),
"deleted": deleted,
"failed": failed if failed else None,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def stop_execution(client: N8nClient, execution_id: str) -> str:
"""Stop a running execution"""
try:
await client.stop_execution(execution_id)
result = {"success": True, "message": f"Execution {execution_id} stopped successfully"}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def retry_execution(client: N8nClient, execution_id: str) -> str:
"""Retry a failed execution"""
try:
# Get the original execution to find workflow ID
original = await client.get_execution(execution_id, include_data=True)
workflow_id = original.get("workflowId")
if not workflow_id:
raise Exception("Cannot find workflow ID from execution")
# Get input data from original execution
input_data = None
exec_data = original.get("data", {})
if exec_data:
# Try to get input from first node
result_data = exec_data.get("resultData", {})
run_data = result_data.get("runData", {})
if run_data:
first_node = list(run_data.keys())[0] if run_data else None
if first_node:
node_data = run_data[first_node]
if node_data and len(node_data) > 0:
input_data = node_data[0].get("data", {}).get("main", [[]])[0]
# Execute the workflow again
new_execution = await client.execute_workflow(workflow_id, data=input_data)
result = {
"success": True,
"message": f"Retrying execution {execution_id}",
"original_execution_id": execution_id,
"new_execution": {
"id": new_execution.get("id"),
"workflow_id": workflow_id,
"status": new_execution.get("status", "running"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_execution_data(client: N8nClient, execution_id: str) -> str:
"""Get full execution data"""
try:
execution = await client.get_execution(execution_id, include_data=True)
exec_data = execution.get("data", {})
result_data = exec_data.get("resultData", {})
result = {
"success": True,
"execution_id": execution_id,
"status": execution.get("status"),
"finished": execution.get("finished"),
"run_data": result_data.get("runData", {}),
"last_node_executed": result_data.get("lastNodeExecuted"),
"error": result_data.get("error"),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def wait_for_execution(
client: N8nClient, execution_id: str, timeout_seconds: int = 60, poll_interval: int = 2
) -> str:
"""Wait for execution to complete"""
try:
elapsed = 0
final_status = None
execution = None
while elapsed < timeout_seconds:
execution = await client.get_execution(execution_id, include_data=True)
status = execution.get("status")
finished = execution.get("finished")
if finished or status in ["success", "error", "crashed"]:
final_status = status
break
await asyncio.sleep(poll_interval)
elapsed += poll_interval
if not final_status:
return json.dumps(
{
"success": False,
"error": f"Timeout after {timeout_seconds} seconds",
"execution_id": execution_id,
"last_status": execution.get("status") if execution else "unknown",
},
indent=2,
)
result = {
"success": True,
"execution_id": execution_id,
"final_status": final_status,
"finished": execution.get("finished"),
"started_at": execution.get("startedAt"),
"stopped_at": execution.get("stoppedAt"),
"duration_seconds": elapsed,
}
# Include error if failed
if final_status == "error":
exec_data = execution.get("data", {})
result_data = exec_data.get("resultData", {})
result["error"] = result_data.get("error")
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,228 @@
"""Projects Handler - manages n8n projects (Enterprise/Pro)"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_projects",
"method_name": "list_projects",
"description": "[Enterprise] List all projects. Requires n8n Enterprise/Pro license. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 250},
"cursor": {"type": "string", "description": "OPTIONAL: Pagination cursor."},
},
},
"scope": "read",
},
{
"name": "get_project",
"method_name": "get_project",
"description": "[Enterprise] Get project details by ID. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {"project_id": {"type": "string", "minLength": 1}},
"required": ["project_id"],
},
"scope": "read",
},
{
"name": "create_project",
"method_name": "create_project",
"description": "[Enterprise] Create a new project. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "description": "Project name"}
},
"required": ["name"],
},
"scope": "admin",
},
{
"name": "update_project",
"method_name": "update_project",
"description": "[Enterprise] Update project metadata. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "minLength": 1},
"name": {"type": "string", "minLength": 1, "description": "New project name"},
},
"required": ["project_id", "name"],
},
"scope": "admin",
},
{
"name": "delete_project",
"method_name": "delete_project",
"description": "[Enterprise] Delete a project. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {"project_id": {"type": "string", "minLength": 1}},
"required": ["project_id"],
},
"scope": "admin",
},
{
"name": "add_project_users",
"method_name": "add_project_users",
"description": "[Enterprise] Add users to a project with roles. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "minLength": 1},
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user_id": {"type": "string"},
"role": {
"type": "string",
"enum": ["project:admin", "project:editor", "project:viewer"],
},
},
"required": ["user_id", "role"],
},
"minItems": 1,
},
},
"required": ["project_id", "users"],
},
"scope": "admin",
},
{
"name": "change_project_user_role",
"method_name": "change_project_user_role",
"description": "[Enterprise] Change a user's role in a project. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "minLength": 1},
"user_id": {"type": "string", "minLength": 1},
"role": {
"type": "string",
"enum": ["project:admin", "project:editor", "project:viewer"],
},
},
"required": ["project_id", "user_id", "role"],
},
"scope": "admin",
},
{
"name": "remove_project_user",
"method_name": "remove_project_user",
"description": "[Enterprise] Remove a user from a project. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "minLength": 1},
"user_id": {"type": "string", "minLength": 1},
},
"required": ["project_id", "user_id"],
},
"scope": "admin",
},
]
async def list_projects(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_projects(limit=limit, cursor=cursor)
projects = response.get("data", [])
result = {
"success": True,
"count": len(projects),
"projects": [{"id": p.get("id"), "name": p.get("name")} for p in projects],
"next_cursor": response.get("nextCursor"),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_project(client: N8nClient, project_id: str) -> str:
try:
project = await client.get_project(project_id)
return json.dumps(
{"success": True, "project": {"id": project.get("id"), "name": project.get("name")}},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_project(client: N8nClient, name: str) -> str:
try:
project = await client.create_project(name)
return json.dumps(
{
"success": True,
"message": f"Project '{name}' created",
"project": {"id": project.get("id"), "name": project.get("name")},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def update_project(client: N8nClient, project_id: str, name: str) -> str:
try:
project = await client.update_project(project_id, name)
return json.dumps(
{
"success": True,
"message": f"Project updated to '{name}'",
"project": {"id": project.get("id"), "name": project.get("name")},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_project(client: N8nClient, project_id: str) -> str:
try:
await client.delete_project(project_id)
return json.dumps({"success": True, "message": f"Project {project_id} deleted"}, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def add_project_users(client: N8nClient, project_id: str, users: list[dict[str, str]]) -> str:
try:
relations = [{"userId": u["user_id"], "role": u["role"]} for u in users]
await client.add_project_users(project_id, relations)
return json.dumps(
{"success": True, "message": f"Added {len(users)} user(s) to project {project_id}"},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def change_project_user_role(
client: N8nClient, project_id: str, user_id: str, role: str
) -> str:
try:
await client.change_project_user_role(project_id, user_id, role)
return json.dumps(
{
"success": True,
"message": f"User {user_id} role changed to {role} in project {project_id}",
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def remove_project_user(client: N8nClient, project_id: str, user_id: str) -> str:
try:
await client.remove_project_user(project_id, user_id)
return json.dumps(
{"success": True, "message": f"User {user_id} removed from project {project_id}"},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,150 @@
"""System Handler - manages n8n system operations (audit, source control, health)"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "run_security_audit",
"method_name": "run_security_audit",
"description": "Run a security audit on the n8n instance. Returns security diagnostics grouped by category. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {"type": "string"},
"description": "OPTIONAL: Specific categories to audit. Omit for all categories.",
}
},
},
"scope": "admin",
},
{
"name": "source_control_pull",
"method_name": "source_control_pull",
"description": "[Enterprise] Pull workflows from source control (Git). Syncs workflows from connected repository. Requires n8n Enterprise/Pro license. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"variables": {
"type": "object",
"description": "OPTIONAL: Variables to set during pull. Omit if not needed.",
},
"force": {
"type": "boolean",
"description": "Force pull even if conflicts exist",
"default": False,
},
},
},
"scope": "admin",
},
{
"name": "get_instance_info",
"method_name": "get_instance_info",
"description": "Get n8n instance information including version and configuration.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "health_check",
"method_name": "health_check",
"description": "Check if the n8n instance is healthy and accessible.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
]
async def run_security_audit(client: N8nClient, categories: list[str] | None = None) -> str:
"""Run security audit"""
try:
result = await client.run_audit(categories)
# Parse audit results
audit_data = {"success": True, "audit_results": result}
# Extract summary if available
if isinstance(result, dict):
risk_report = result.get("risk", {})
if risk_report:
audit_data["summary"] = {
"risk_categories": list(risk_report.keys()),
"total_issues": sum(
len(issues) if isinstance(issues, list) else 0
for issues in risk_report.values()
),
}
return json.dumps(audit_data, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def source_control_pull(
client: N8nClient, variables: dict[str, str] | None = None, force: bool = False
) -> str:
"""Pull from source control"""
try:
result = await client.source_control_pull(variables=variables, force=force)
return json.dumps(
{"success": True, "message": "Source control pull completed", "result": result},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_instance_info(client: N8nClient) -> str:
"""Get instance information"""
try:
# Try multiple endpoints to gather instance info
info = {}
# Get version info from settings or health
try:
health = await client.health_check()
info["health"] = health
except:
pass
# Get current user to verify connectivity
try:
user = await client.request("GET", "user")
info["current_user"] = {
"id": user.get("id"),
"email": user.get("email"),
"role": user.get("role"),
}
except:
pass
info["instance_url"] = client.site_url
info["api_base"] = client.api_base
return json.dumps({"success": True, "instance_info": info}, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def health_check(client: N8nClient) -> str:
"""Check instance health"""
try:
result = await client.health_check()
return json.dumps(
{
"success": True,
"healthy": result.get("healthy", False),
"status": result.get("status", "unknown"),
"instance_url": client.site_url,
},
indent=2,
)
except Exception as e:
return json.dumps(
{"success": False, "healthy": False, "error": str(e), "instance_url": client.site_url},
indent=2,
)

View File

@@ -0,0 +1,160 @@
"""Tags Handler - manages n8n tags"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_tags",
"method_name": "list_tags",
"description": "List all tags used for workflow organization. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 250},
"cursor": {"type": "string", "description": "OPTIONAL: Pagination cursor."},
},
},
"scope": "read",
},
{
"name": "get_tag",
"method_name": "get_tag",
"description": "Get tag details by ID.",
"schema": {
"type": "object",
"properties": {"tag_id": {"type": "string", "minLength": 1}},
"required": ["tag_id"],
},
"scope": "read",
},
{
"name": "create_tag",
"method_name": "create_tag",
"description": "Create a new tag for workflow organization.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "description": "Tag name"}
},
"required": ["name"],
},
"scope": "write",
},
{
"name": "update_tag",
"method_name": "update_tag",
"description": "Update tag name.",
"schema": {
"type": "object",
"properties": {
"tag_id": {"type": "string", "minLength": 1},
"name": {"type": "string", "minLength": 1, "description": "New tag name"},
},
"required": ["tag_id", "name"],
},
"scope": "write",
},
{
"name": "delete_tag",
"method_name": "delete_tag",
"description": "Delete a tag.",
"schema": {
"type": "object",
"properties": {"tag_id": {"type": "string", "minLength": 1}},
"required": ["tag_id"],
},
"scope": "write",
},
{
"name": "delete_tags",
"method_name": "delete_tags",
"description": "Bulk delete multiple tags.",
"schema": {
"type": "object",
"properties": {
"tag_ids": {"type": "array", "items": {"type": "string"}, "minItems": 1}
},
"required": ["tag_ids"],
},
"scope": "write",
},
]
async def list_tags(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_tags(limit=limit, cursor=cursor)
tags = response.get("data", [])
result = {
"success": True,
"count": len(tags),
"tags": [{"id": t.get("id"), "name": t.get("name")} for t in tags],
"next_cursor": response.get("nextCursor"),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_tag(client: N8nClient, tag_id: str) -> str:
try:
tag = await client.get_tag(tag_id)
return json.dumps(
{"success": True, "tag": {"id": tag.get("id"), "name": tag.get("name")}}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_tag(client: N8nClient, name: str) -> str:
try:
tag = await client.create_tag(name)
return json.dumps(
{
"success": True,
"message": f"Tag '{name}' created",
"tag": {"id": tag.get("id"), "name": tag.get("name")},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def update_tag(client: N8nClient, tag_id: str, name: str) -> str:
try:
tag = await client.update_tag(tag_id, name)
return json.dumps(
{
"success": True,
"message": f"Tag updated to '{name}'",
"tag": {"id": tag.get("id"), "name": tag.get("name")},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_tag(client: N8nClient, tag_id: str) -> str:
try:
await client.delete_tag(tag_id)
return json.dumps({"success": True, "message": f"Tag {tag_id} deleted"}, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_tags(client: N8nClient, tag_ids: list[str]) -> str:
try:
deleted, failed = [], []
for tid in tag_ids:
try:
await client.delete_tag(tid)
deleted.append(tid)
except Exception as e:
failed.append({"id": tid, "error": str(e)})
return json.dumps(
{"success": len(failed) == 0, "deleted": deleted, "failed": failed if failed else None},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,167 @@
"""Users Handler - manages n8n users"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_users",
"method_name": "list_users",
"description": "List all users in the n8n instance. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 250},
"cursor": {"type": "string", "description": "OPTIONAL: Pagination cursor."},
"include_role": {"type": "boolean", "default": True},
},
},
"scope": "admin",
},
{
"name": "get_user",
"method_name": "get_user",
"description": "Get user details by ID or email.",
"schema": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"minLength": 1,
"description": "User ID or email",
},
"include_role": {"type": "boolean", "default": True},
},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "create_user",
"method_name": "create_user",
"description": "Invite/create a new user.",
"schema": {
"type": "object",
"properties": {
"email": {"type": "string", "format": "email", "description": "User email"},
"role": {
"type": "string",
"enum": ["global:owner", "global:admin", "global:member"],
"default": "global:member",
},
},
"required": ["email"],
},
"scope": "admin",
},
{
"name": "delete_user",
"method_name": "delete_user",
"description": "Delete a user from the instance.",
"schema": {
"type": "object",
"properties": {"user_id": {"type": "string", "minLength": 1}},
"required": ["user_id"],
},
"scope": "admin",
},
{
"name": "change_user_role",
"method_name": "change_user_role",
"description": "Change a user's global role.",
"schema": {
"type": "object",
"properties": {
"user_id": {"type": "string", "minLength": 1},
"new_role": {
"type": "string",
"enum": ["global:owner", "global:admin", "global:member"],
"description": "New role for the user",
},
},
"required": ["user_id", "new_role"],
},
"scope": "admin",
},
]
async def list_users(
client: N8nClient, limit: int = 100, cursor: str | None = None, include_role: bool = True
) -> str:
try:
response = await client.list_users(limit=limit, cursor=cursor, include_role=include_role)
users = response.get("data", [])
result = {
"success": True,
"count": len(users),
"users": [
{
"id": u.get("id"),
"email": u.get("email"),
"first_name": u.get("firstName"),
"last_name": u.get("lastName"),
"role": u.get("role"),
"is_pending": u.get("isPending"),
}
for u in users
],
"next_cursor": response.get("nextCursor"),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_user(client: N8nClient, user_id: str, include_role: bool = True) -> str:
try:
user = await client.get_user(user_id, include_role)
return json.dumps(
{
"success": True,
"user": {
"id": user.get("id"),
"email": user.get("email"),
"first_name": user.get("firstName"),
"last_name": user.get("lastName"),
"role": user.get("role"),
"is_pending": user.get("isPending"),
},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_user(client: N8nClient, email: str, role: str = "global:member") -> str:
try:
users = [{"email": email, "role": role}]
response = await client.create_user(users)
return json.dumps(
{
"success": True,
"message": f"User {email} invited successfully",
"users": response.get("data", []),
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_user(client: N8nClient, user_id: str) -> str:
try:
await client.delete_user(user_id)
return json.dumps({"success": True, "message": f"User {user_id} deleted"}, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def change_user_role(client: N8nClient, user_id: str, new_role: str) -> str:
try:
await client.change_user_role(user_id, new_role)
return json.dumps(
{"success": True, "message": f"User {user_id} role changed to {new_role}"}, indent=2
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,186 @@
"""Variables Handler - manages n8n environment variables"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_variables",
"method_name": "list_variables",
"description": "[Enterprise] List all environment variables. Requires n8n Enterprise/Pro license. All parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 250},
"cursor": {"type": "string", "description": "OPTIONAL: Pagination cursor."},
},
},
"scope": "read",
},
{
"name": "get_variable",
"method_name": "get_variable",
"description": "[Enterprise] Get variable value by key. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"key": {"type": "string", "minLength": 1, "description": "Variable key"}
},
"required": ["key"],
},
"scope": "read",
},
{
"name": "create_variable",
"method_name": "create_variable",
"description": "[Enterprise] Create a new environment variable. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"key": {"type": "string", "minLength": 1, "description": "Variable key"},
"value": {"type": "string", "description": "Variable value"},
},
"required": ["key", "value"],
},
"scope": "admin",
},
{
"name": "update_variable",
"method_name": "update_variable",
"description": "[Enterprise] Update an existing variable's value. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"key": {"type": "string", "minLength": 1},
"value": {"type": "string", "description": "New value"},
},
"required": ["key", "value"],
},
"scope": "admin",
},
{
"name": "delete_variable",
"method_name": "delete_variable",
"description": "[Enterprise] Delete an environment variable. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {"key": {"type": "string", "minLength": 1}},
"required": ["key"],
},
"scope": "admin",
},
{
"name": "set_variables",
"method_name": "set_variables",
"description": "[Enterprise] Bulk set multiple variables at once. Requires n8n Enterprise/Pro license.",
"schema": {
"type": "object",
"properties": {
"variables": {
"type": "object",
"description": "Key-value pairs of variables to set",
"additionalProperties": {"type": "string"},
}
},
"required": ["variables"],
},
"scope": "admin",
},
]
async def list_variables(client: N8nClient, limit: int = 100, cursor: str | None = None) -> str:
try:
response = await client.list_variables(limit=limit, cursor=cursor)
variables = response.get("data", [])
result = {
"success": True,
"count": len(variables),
"variables": [{"key": v.get("key"), "value": v.get("value")} for v in variables],
"next_cursor": response.get("nextCursor"),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_variable(client: N8nClient, key: str) -> str:
try:
variable = await client.get_variable(key)
return json.dumps(
{
"success": True,
"variable": {"key": variable.get("key"), "value": variable.get("value")},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_variable(client: N8nClient, key: str, value: str) -> str:
try:
await client.create_variable(key, value)
return json.dumps(
{
"success": True,
"message": f"Variable '{key}' created",
"variable": {"key": key, "value": value},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def update_variable(client: N8nClient, key: str, value: str) -> str:
try:
await client.update_variable(key, value)
return json.dumps(
{
"success": True,
"message": f"Variable '{key}' updated",
"variable": {"key": key, "value": value},
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_variable(client: N8nClient, key: str) -> str:
try:
await client.delete_variable(key)
return json.dumps({"success": True, "message": f"Variable '{key}' deleted"}, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def set_variables(client: N8nClient, variables: dict[str, str]) -> str:
try:
created, updated, failed = [], [], []
for key, value in variables.items():
try:
# Try to get existing variable
try:
await client.get_variable(key)
# Variable exists, update it
await client.update_variable(key, value)
updated.append(key)
except:
# Variable doesn't exist, create it
await client.create_variable(key, value)
created.append(key)
except Exception as e:
failed.append({"key": key, "error": str(e)})
return json.dumps(
{
"success": len(failed) == 0,
"created": created,
"updated": updated,
"failed": failed if failed else None,
},
indent=2,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

View File

@@ -0,0 +1,680 @@
"""Workflow Handler - manages n8n workflows"""
import json
from typing import Any
from plugins.n8n.client import N8nClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === LIST WORKFLOWS ===
{
"name": "list_workflows",
"method_name": "list_workflows",
"description": "List all n8n workflows with optional filters. Returns workflow ID, name, active status, tags, and metadata. All filter parameters are OPTIONAL.",
"schema": {
"type": "object",
"properties": {
"active": {
"type": "boolean",
"description": "OPTIONAL: Filter by active/inactive status. Omit for all workflows.",
},
"tags": {
"type": "string",
"description": "OPTIONAL: Filter by tag name(s), comma-separated. Omit for all workflows.",
},
"name": {
"type": "string",
"description": "OPTIONAL: Filter by workflow name (partial match). Omit for all workflows.",
},
"limit": {
"type": "integer",
"description": "Maximum workflows to return",
"default": 50,
"minimum": 1,
"maximum": 250,
},
"cursor": {
"type": "string",
"description": "OPTIONAL: Pagination cursor for next page.",
},
},
},
"scope": "read",
},
# === GET WORKFLOW ===
{
"name": "get_workflow",
"method_name": "get_workflow",
"description": "Get detailed information about a specific workflow including nodes, connections, and settings.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {"type": "string", "description": "Workflow ID", "minLength": 1}
},
"required": ["workflow_id"],
},
"scope": "read",
},
# === CREATE WORKFLOW ===
{
"name": "create_workflow",
"method_name": "create_workflow",
"description": "Create a new workflow from JSON definition. Workflow will be inactive by default. Note: settings and static_data are OPTIONAL - omit them entirely if not needed.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Workflow name", "minLength": 1},
"nodes": {
"type": "array",
"description": "Array of node definitions",
"items": {"type": "object"},
"default": [],
},
"connections": {
"type": "object",
"description": "Node connections definition",
"default": {},
},
"settings": {
"type": "object",
"description": "OPTIONAL: Workflow settings (timezone, error workflow, etc.). Omit this parameter if not needed.",
"default": {},
},
"static_data": {
"type": "object",
"description": "OPTIONAL: Static data for the workflow. Omit this parameter if not needed.",
"default": {},
},
},
"required": ["name"],
},
"scope": "write",
},
# === UPDATE WORKFLOW ===
{
"name": "update_workflow",
"method_name": "update_workflow",
"description": "Update an existing workflow. Can modify nodes, connections, settings, or name. All parameters except workflow_id are OPTIONAL - omit them to keep current values.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to update",
"minLength": 1,
},
"name": {
"type": "string",
"description": "OPTIONAL: New workflow name. Omit to keep current.",
},
"nodes": {
"type": "array",
"items": {"type": "object"},
"description": "OPTIONAL: Updated node definitions. Omit to keep current.",
},
"connections": {
"type": "object",
"description": "OPTIONAL: Updated connections. Omit to keep current.",
},
"settings": {
"type": "object",
"description": "OPTIONAL: Updated settings. Omit to keep current.",
},
},
"required": ["workflow_id"],
},
"scope": "write",
},
# === DELETE WORKFLOW ===
{
"name": "delete_workflow",
"method_name": "delete_workflow",
"description": "Permanently delete a workflow. This action cannot be undone.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to delete",
"minLength": 1,
}
},
"required": ["workflow_id"],
},
"scope": "admin",
},
# === ACTIVATE WORKFLOW ===
{
"name": "activate_workflow",
"method_name": "activate_workflow",
"description": "Activate a workflow so it runs automatically when triggered. Workflow must have at least one trigger node.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to activate",
"minLength": 1,
}
},
"required": ["workflow_id"],
},
"scope": "write",
},
# === DEACTIVATE WORKFLOW ===
{
"name": "deactivate_workflow",
"method_name": "deactivate_workflow",
"description": "Deactivate a workflow. It will no longer run automatically but can still be executed manually.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to deactivate",
"minLength": 1,
}
},
"required": ["workflow_id"],
},
"scope": "write",
},
# === EXECUTE WORKFLOW ===
{
"name": "execute_workflow",
"method_name": "execute_workflow",
"description": "Manually execute a workflow and return execution ID. Use get_execution to check status and results.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to execute",
"minLength": 1,
}
},
"required": ["workflow_id"],
},
"scope": "write",
},
# === EXECUTE WORKFLOW WITH DATA ===
{
"name": "execute_workflow_with_data",
"method_name": "execute_workflow_with_data",
"description": "Execute workflow with custom input data. Useful for workflows with webhook or manual triggers.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to execute",
"minLength": 1,
},
"data": {
"type": "object",
"description": "Input data to pass to workflow trigger node",
},
},
"required": ["workflow_id", "data"],
},
"scope": "write",
},
# === DUPLICATE WORKFLOW ===
{
"name": "duplicate_workflow",
"method_name": "duplicate_workflow",
"description": "Create a copy of an existing workflow with a new name.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to duplicate",
"minLength": 1,
},
"new_name": {
"type": "string",
"description": "Name for the duplicated workflow",
"minLength": 1,
},
},
"required": ["workflow_id", "new_name"],
},
"scope": "write",
},
# === EXPORT WORKFLOW ===
{
"name": "export_workflow",
"method_name": "export_workflow",
"description": "Export a workflow as JSON. Can be used for backup or sharing.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {
"type": "string",
"description": "Workflow ID to export",
"minLength": 1,
}
},
"required": ["workflow_id"],
},
"scope": "read",
},
# === IMPORT WORKFLOW ===
{
"name": "import_workflow",
"method_name": "import_workflow",
"description": "Import a workflow from JSON definition. Similar to create but accepts full workflow JSON.",
"schema": {
"type": "object",
"properties": {
"workflow_json": {
"type": "object",
"description": "Full workflow JSON to import",
},
"name_override": {
"type": "string",
"description": "OPTIONAL: Override the workflow name from JSON. Omit to use the name from JSON.",
},
},
"required": ["workflow_json"],
},
"scope": "write",
},
# === GET WORKFLOW TAGS ===
{
"name": "get_workflow_tags",
"method_name": "get_workflow_tags",
"description": "Get list of tags assigned to a workflow.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {"type": "string", "description": "Workflow ID", "minLength": 1}
},
"required": ["workflow_id"],
},
"scope": "read",
},
# === SET WORKFLOW TAGS ===
{
"name": "set_workflow_tags",
"method_name": "set_workflow_tags",
"description": "Assign tags to a workflow. Replaces existing tags. Pass empty array [] to remove all tags.",
"schema": {
"type": "object",
"properties": {
"workflow_id": {"type": "string", "description": "Workflow ID", "minLength": 1},
"tag_ids": {
"type": "array",
"description": "List of tag IDs to assign. Use empty array [] to remove all tags. Get tag IDs using n8n_list_tags first.",
"items": {"type": "string"},
"default": [],
},
},
"required": ["workflow_id"],
},
"scope": "write",
},
]
# === HANDLER FUNCTIONS ===
async def list_workflows(
client: N8nClient,
active: bool | None = None,
tags: str | None = None,
name: str | None = None,
limit: int = 50,
cursor: str | None = None,
) -> str:
"""List all workflows with filters"""
try:
response = await client.list_workflows(
active=active, tags=tags, name=name, limit=limit, cursor=cursor
)
workflows = response.get("data", [])
next_cursor = response.get("nextCursor")
result = {
"success": True,
"count": len(workflows),
"workflows": [
{
"id": w.get("id"),
"name": w.get("name"),
"active": w.get("active"),
"tags": [t.get("name") for t in w.get("tags", [])],
"created_at": w.get("createdAt"),
"updated_at": w.get("updatedAt"),
}
for w in workflows
],
"next_cursor": next_cursor,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_workflow(client: N8nClient, workflow_id: str) -> str:
"""Get workflow details"""
try:
workflow = await client.get_workflow(workflow_id)
result = {
"success": True,
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active"),
"nodes": workflow.get("nodes", []),
"connections": workflow.get("connections", {}),
"settings": workflow.get("settings", {}),
"static_data": workflow.get("staticData"),
"tags": [t.get("name") for t in workflow.get("tags", [])],
"created_at": workflow.get("createdAt"),
"updated_at": workflow.get("updatedAt"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def create_workflow(
client: N8nClient,
name: str,
nodes: list[dict[str, Any]] | None = None,
connections: dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
static_data: dict[str, Any] | None = None,
) -> str:
"""Create a new workflow"""
try:
# Use defaults for optional parameters
data = {
"name": name,
"nodes": nodes if nodes is not None else [],
"connections": connections if connections is not None else {},
}
# Only add settings/static_data if provided and non-empty
if settings and isinstance(settings, dict) and len(settings) > 0:
data["settings"] = settings
if static_data and isinstance(static_data, dict) and len(static_data) > 0:
data["staticData"] = static_data
workflow = await client.create_workflow(data)
result = {
"success": True,
"message": f"Workflow '{name}' created successfully",
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active", False),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def update_workflow(
client: N8nClient,
workflow_id: str,
name: str | None = None,
nodes: list[dict[str, Any]] | None = None,
connections: dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
) -> str:
"""Update an existing workflow"""
try:
# Get current workflow first
current = await client.get_workflow(workflow_id)
# Build update data
data = {
"name": name or current.get("name"),
"nodes": nodes or current.get("nodes", []),
"connections": connections or current.get("connections", {}),
}
if settings:
data["settings"] = settings
workflow = await client.update_workflow(workflow_id, data)
result = {
"success": True,
"message": f"Workflow '{workflow.get('name')}' updated successfully",
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def delete_workflow(client: N8nClient, workflow_id: str) -> str:
"""Delete a workflow"""
try:
await client.delete_workflow(workflow_id)
result = {"success": True, "message": f"Workflow {workflow_id} deleted successfully"}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def activate_workflow(client: N8nClient, workflow_id: str) -> str:
"""Activate a workflow"""
try:
workflow = await client.activate_workflow(workflow_id)
result = {
"success": True,
"message": f"Workflow '{workflow.get('name')}' activated successfully",
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def deactivate_workflow(client: N8nClient, workflow_id: str) -> str:
"""Deactivate a workflow"""
try:
workflow = await client.deactivate_workflow(workflow_id)
result = {
"success": True,
"message": f"Workflow '{workflow.get('name')}' deactivated successfully",
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def execute_workflow(client: N8nClient, workflow_id: str) -> str:
"""Execute a workflow manually"""
try:
execution = await client.execute_workflow(workflow_id)
result = {
"success": True,
"message": "Workflow execution started",
"execution": {
"id": execution.get("id"),
"workflow_id": workflow_id,
"status": execution.get("status", "running"),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def execute_workflow_with_data(
client: N8nClient, workflow_id: str, data: dict[str, Any]
) -> str:
"""Execute workflow with custom input data"""
try:
execution = await client.execute_workflow(workflow_id, data=data)
result = {
"success": True,
"message": "Workflow execution started with custom data",
"execution": {
"id": execution.get("id"),
"workflow_id": workflow_id,
"status": execution.get("status", "running"),
"input_data": data,
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def duplicate_workflow(client: N8nClient, workflow_id: str, new_name: str) -> str:
"""Duplicate a workflow"""
try:
# Get the original workflow
original = await client.get_workflow(workflow_id)
# Create new workflow with same structure
data = {
"name": new_name,
"nodes": original.get("nodes", []),
"connections": original.get("connections", {}),
"settings": original.get("settings", {}),
}
new_workflow = await client.create_workflow(data)
result = {
"success": True,
"message": f"Workflow duplicated as '{new_name}'",
"original": {"id": original.get("id"), "name": original.get("name")},
"duplicate": {"id": new_workflow.get("id"), "name": new_workflow.get("name")},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def export_workflow(client: N8nClient, workflow_id: str) -> str:
"""Export workflow as JSON"""
try:
workflow = await client.get_workflow(workflow_id)
# Return full workflow JSON for export
result = {"success": True, "workflow_json": workflow}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def import_workflow(
client: N8nClient, workflow_json: dict[str, Any], name_override: str | None = None
) -> str:
"""Import workflow from JSON"""
try:
# Use name override if provided
if name_override:
workflow_json["name"] = name_override
# Ensure required fields
if "nodes" not in workflow_json:
workflow_json["nodes"] = []
if "connections" not in workflow_json:
workflow_json["connections"] = {}
workflow = await client.create_workflow(workflow_json)
result = {
"success": True,
"message": f"Workflow '{workflow.get('name')}' imported successfully",
"workflow": {
"id": workflow.get("id"),
"name": workflow.get("name"),
"active": workflow.get("active", False),
},
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def get_workflow_tags(client: N8nClient, workflow_id: str) -> str:
"""Get tags assigned to a workflow"""
try:
tags = await client.get_workflow_tags(workflow_id)
result = {"success": True, "workflow_id": workflow_id, "tags": tags}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)
async def set_workflow_tags(
client: N8nClient, workflow_id: str, tag_ids: list[str] | None = None
) -> str:
"""Set tags for a workflow"""
try:
# Use empty list if tag_ids is None
tags_to_set = tag_ids if tag_ids is not None else []
# Get current workflow
current = await client.get_workflow(workflow_id)
# Update with new tags
data = {
"name": current.get("name"),
"nodes": current.get("nodes", []),
"connections": current.get("connections", {}),
"tags": [{"id": tid} for tid in tags_to_set],
}
workflow = await client.update_workflow(workflow_id, data)
result = {
"success": True,
"message": f"Tags updated for workflow {workflow_id}",
"workflow_id": workflow_id,
"tags": [t.get("name") for t in workflow.get("tags", [])],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2)

145
plugins/n8n/plugin.py Normal file
View File

@@ -0,0 +1,145 @@
"""
n8n Plugin - Workflow Automation Management
Complete n8n workflow automation management through REST API.
Provides 56 tools across 8 categories: workflows, executions,
credentials, tags, users, projects, variables, and system.
"""
from typing import Any
from plugins.base import BasePlugin
from plugins.n8n import handlers
from plugins.n8n.client import N8nClient
class N8nPlugin(BasePlugin):
"""
n8n Automation Plugin - Comprehensive workflow management.
Provides complete n8n management capabilities including:
- Workflow management (CRUD, activate, deactivate, execute)
- Execution monitoring (list, get, delete, retry, wait)
- Credential management (get, create, delete, schema, transfer)
- Tag management (CRUD, bulk delete)
- User management (CRUD, roles)
- Project management (CRUD, user assignment) - Enterprise/Pro
- Variable management (CRUD, bulk set) - Enterprise/Pro
- System operations (audit, source control, health)
Total: 56 tools
"""
@staticmethod
def get_plugin_name() -> str:
"""Return plugin type identifier"""
return "n8n"
@staticmethod
def get_required_config_keys() -> list[str]:
"""Return required configuration keys"""
return ["url", "api_key"]
def __init__(self, config: dict[str, Any], project_id: str | None = None):
"""
Initialize n8n plugin with client.
Args:
config: Configuration dictionary containing:
- url: n8n instance URL
- api_key: n8n API key
project_id: Optional project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
# Create n8n API client
self.client = N8nClient(site_url=config["url"], api_key=config["api_key"])
@staticmethod
def get_tool_specifications() -> list[dict[str, Any]]:
"""
Return all tool specifications for ToolGenerator.
This method is called by ToolGenerator to create unified tools
with site parameter routing.
Returns:
List of tool specification dictionaries (56 tools total)
"""
specs = []
# Collect specifications from all handlers
specs.extend(handlers.workflows.get_tool_specifications()) # 14 tools
specs.extend(handlers.executions.get_tool_specifications()) # 8 tools
specs.extend(handlers.credentials.get_tool_specifications()) # 5 tools
specs.extend(handlers.tags.get_tool_specifications()) # 6 tools
specs.extend(handlers.users.get_tool_specifications()) # 5 tools
specs.extend(handlers.projects.get_tool_specifications()) # 8 tools [Enterprise]
specs.extend(handlers.variables.get_tool_specifications()) # 6 tools [Enterprise]
specs.extend(handlers.system.get_tool_specifications()) # 4 tools
return specs
def __getattr__(self, name: str):
"""
Dynamically delegate method calls to appropriate handlers.
This allows ToolGenerator to call methods like plugin.list_workflows()
without explicitly defining each method.
Args:
name: Method name being called
Returns:
Handler function from the appropriate handler module
"""
# Try to find the method in handler modules
handler_modules = [
handlers.workflows,
handlers.executions,
handlers.credentials,
handlers.tags,
handlers.users,
handlers.projects,
handlers.variables,
handlers.system,
]
for handler_module in handler_modules:
if hasattr(handler_module, name):
func = getattr(handler_module, name)
# Create wrapper that passes self.client
async def wrapper(_func=func, **kwargs):
return await _func(self.client, **kwargs)
return wrapper
# Method not found in any handler
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
async def check_health(self) -> dict[str, Any]:
"""
Check if n8n instance is accessible (internal use).
Note: This is named check_health to avoid shadowing the handler's
health_check function which is exposed as an MCP tool.
Returns:
Dict containing health check result
"""
try:
result = await self.client.health_check()
return {
"healthy": result.get("healthy", False),
"message": f"n8n instance at {self.client.site_url} is {'accessible' if result.get('healthy') else 'not accessible'}",
}
except Exception as e:
return {"healthy": False, "message": f"n8n health check failed: {str(e)}"}
async def health_check(self, **kwargs) -> str:
"""
Override BasePlugin.health_check to use handler function.
This ensures the MCP tool returns a JSON string, not a Dict.
"""
return await handlers.system.health_check(self.client)