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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Diese Datei ist der Index; die Tiefe liegt in `docs/`:
- **Remote Execution:** PowerShell SDK / WinRM, agentless. `Remote:Provider`: `winrm` (default) | `noop` (`noop` muss per `Remote:AllowNoop=true` bzw. `NODEPILOT_ALLOW_NOOP_REMOTE=1` quittiert werden, sonst Boot-Abbruch). Engine-local (In-Proc-Pool): implizite WinPS-Kompatibilität **deaktiviert** (Desktop-only-Module → lauter Fehler statt `powershell.exe -s`-Session-Leak; `Microsoft.PowerShell.Archive` gebündelt) — Details `docs/claude-reference.md` + `docs/performance-improvements.md`
- **Real-time:** SignalR (`/hubs/execution`)
- **Logging:** Serilog. Format via `Logging:Format`: `text`|`cmtrace`|`json`|`ecs-json` (ECS 1.x für SIEM, siehe `docs/siem-logging.md`). Support-Log: File + DB-Projektion
- **MCP-Server (opt-in):** `nodepilot-mcp` (stdio) — AI-Agent steuert/editiert Workflows über 99 Tools, HTTP-only gegen die REST-API
- **MCP-Server (opt-in):** `nodepilot-mcp` (stdio) — AI-Agent steuert/editiert Workflows über 100 Tools, HTTP-only gegen die REST-API
- **Enterprise (opt-in):** Active/Passive HA (`Cluster:Enabled`), pluggable Secret-Provider (`Secrets:Provider` = `Dpapi`|`AesGcm`), LDAP/Windows-SSO, ECS-JSON-SIEM, Folder-RBAC

## Solution-Struktur
Expand Down Expand Up @@ -333,7 +333,7 @@ Scoped Testing übersieht genau eine Fehlerklasse — die Parity-/Drift-Tests, d

## Clients (`np` CLI + `nodepilot-mcp`)

Beide sind reine HTTP-Clients gegen die REST-API — **kein** eigener Backend-Pfad. Ausgeliefert werden beide per `dotnet publish` (Ordner in den `PATH` bzw. `.mcp.json` auf die `.exe` zeigen lassen); **keine** `dotnet global tool`s — `PackAsTool` verträgt das geerbte `net10.0-windows`-TFM nicht (NETSDK1146, siehe `docs/roadmap.md`-Sperrvermerk). Der MCP-Server ergänzt In-Proc-Analyse gegen `NodePilot.Core` (99 Tools, 3 Resources, stdio) und reused die DPAPI-Session der CLI (`np auth login`).
Beide sind reine HTTP-Clients gegen die REST-API — **kein** eigener Backend-Pfad. Ausgeliefert werden beide per `dotnet publish` (Ordner in den `PATH` bzw. `.mcp.json` auf die `.exe` zeigen lassen); **keine** `dotnet global tool`s — `PackAsTool` verträgt das geerbte `net10.0-windows`-TFM nicht (NETSDK1146, siehe `docs/roadmap.md`-Sperrvermerk). Der MCP-Server ergänzt In-Proc-Analyse gegen `NodePilot.Core` (100 Tools, 3 Resources, stdio) und reused die DPAPI-Session der CLI (`np auth login`).

**Jeder neue API-Endpoint braucht beide Clients.** Mechanik, Befehlsbereiche und Tool-Katalog: `src/NodePilot.Cli/CLAUDE.md`, `src/NodePilot.Mcp/CLAUDE.md`, `docs/mcp-server.md`, `docs/claude-reference.md`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ it never permits plaintext connections to remote hosts.

## `nodepilot-mcp` — the MCP server

A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an AI agent (Claude Desktop/Code, or any MCP client) **drive and edit workflows and read data** — 99 tools over the same REST API the SPA uses, plus in-process graph/data-bus analysis for the in-canvas chat assistant. Like the CLI: **HTTP-only, no backend dependencies**, and it reuses the CLI's `np auth login` session.
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an AI agent (Claude Desktop/Code, or any MCP client) **drive and edit workflows and read data** — 100 tools over the same REST API the SPA uses, plus in-process graph/data-bus analysis for the in-canvas chat assistant. Like the CLI: **HTTP-only, no backend dependencies**, and it reuses the CLI's `np auth login` session.

```powershell
dotnet publish src/NodePilot.Mcp -c Release -o C:\Tools\NodePilot-Mcp
Expand Down
3 changes: 2 additions & 1 deletion docs/alerting.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ operand mode; the globals query is disabled in that mode). A **Deliveries** butt
### CLI

```
np alerting catalog # event types, filter fields, channels
np alerting list
np alerting get <rule-id>
np alerting create --name "Prod failures" --event-types ExecutionFailed,ExecutionCancelled \
Expand All @@ -200,7 +201,7 @@ Routes are given with `--email` / `--webhook` (repeatable); scope targets with `

### MCP

`list_alerting_rules`, `get_alerting_rule`, `list_alerting_deliveries`, `create_alerting_rule`,
`get_alerting_catalog`, `list_alerting_rules`, `get_alerting_rule`, `list_alerting_deliveries`, `create_alerting_rule`,
`update_alerting_rule`, `test_fire_alerting_rule` (read + write); `delete_alerting_rule` lives in the
gated `DestructiveTools` (only registered when `NODEPILOT_MCP_ALLOW_DESTRUCTIVE=true`; listed in
`get_safety_status`). Route secrets are never surfaced in tool output.
Expand Down
6 changes: 3 additions & 3 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Large free-text fields (stdout/stderr, return data, audit details, diagnostics)

## Tool catalog

89 default tools across 10 groups, plus 10 gated destructive tools (99 total). (Roles refer to the
90 default tools across 10 groups, plus 10 gated destructive tools (100 total). (Roles refer to the
authenticated user.)

### Discovery
Expand Down Expand Up @@ -159,8 +159,8 @@ harmlosen Casts auf diesen Secret-Tabellen; explizit benannte, nicht geschützte
`move_global_variable_folder` · `move_global_variable_to_folder`

### Alerting (notification rules; route secrets never surfaced)
`list_alerting_rules` · `get_alerting_rule` · `create_alerting_rule` · `update_alerting_rule` ·
`test_fire_alerting_rule` · `list_alerting_deliveries`
`get_alerting_catalog` · `list_alerting_rules` · `get_alerting_rule` · `create_alerting_rule` ·
`update_alerting_rule` · `test_fire_alerting_rule` · `list_alerting_deliveries`

### System alerts (ADR 0008; catalog-driven policies)
`get_system_alert_catalog` · `list_system_alert_policies` · `get_system_alert_policy` ·
Expand Down
13 changes: 13 additions & 0 deletions src/NodePilot.Cli/Api/Dtos/AlertingDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ public sealed record TestFireRouteResult(string Channel, string Target, bool Suc

public sealed record TestFireResponse(bool AllSucceeded, List<TestFireRouteResult> Results);

// The rule-authoring catalog: which event types exist, which fields a filter may reference, and
// which channels this installation can actually deliver on. `np alerting create` takes a rule as
// JSON, so without this the field names had to be read out of the web UI or the source.
public sealed record AlertingCatalogFieldDto(string Name, string Applies, string Type, IReadOnlyList<string>? Values = null);

public sealed record AlertingCatalogEventTypeDto(string Name, string Category, bool Scopeable);

public sealed record AlertingCatalogResponse(
IReadOnlyList<AlertingCatalogEventTypeDto> EventTypes,
IReadOnlyList<AlertingCatalogFieldDto> EventFields,
IReadOnlyList<string> Channels,
IReadOnlyList<string> DedupTemplateFields);

public sealed record NotificationDeliveryDto(
Guid Id, Guid RuleId, string? RuleName, Guid RouteId, string? Channel, string? Target,
string EventKey, string Status, int Attempt, DateTime CreatedAt, DateTime? SentAt,
Expand Down
16 changes: 16 additions & 0 deletions src/NodePilot.Cli/Api/NodePilotApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public async Task<WorkflowResponse> GetWorkflowAsync(Guid id, CancellationToken
return await ParseAsync<WorkflowResponse>(res, ct);
}

/// <summary>
/// Name lookup with the engine's own rules: exact case wins, otherwise case-insensitive,
/// ambiguous names answer 409. Listing and filtering client-side cannot reproduce that.
/// </summary>
public async Task<WorkflowResponse> GetWorkflowByNameAsync(string name, CancellationToken ct)
{
using var res = await _http.GetAsync($"api/workflows/by-name/{Uri.EscapeDataString(name)}", ct);
return await ParseAsync<WorkflowResponse>(res, ct);
}

public async Task<ExecutionResponse> ExecuteWorkflowAsync(Guid id, ExecuteWorkflowRequest req, CancellationToken ct)
{
using var res = await _http.PostAsJsonAsync($"api/workflows/{id}/execute", req, JsonOptions, ct);
Expand Down Expand Up @@ -475,6 +485,12 @@ public async Task<List<NotificationRuleResponse>> ListAlertingRulesAsync(Cancell
return await ParseAsync<List<NotificationRuleResponse>>(res, ct);
}

public async Task<AlertingCatalogResponse> GetAlertingCatalogAsync(CancellationToken ct)
{
using var res = await _http.GetAsync("api/alerting/catalog", ct);
return await ParseAsync<AlertingCatalogResponse>(res, ct);
}

public async Task<NotificationRuleResponse> GetAlertingRuleAsync(Guid id, CancellationToken ct)
{
using var res = await _http.GetAsync($"api/alerting/rules/{id}", ct);
Expand Down
1 change: 1 addition & 0 deletions src/NodePilot.Cli/CommandRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ public static void Register(IConfigurator config)
config.AddBranch("alerting", a =>
{
a.SetDescription("Manage alerting rules (notify on matching events).");
a.AddCommand<AlertingCatalogCommand>("catalog").WithDescription("Show the rule vocabulary: event types, filter fields, channels.");
a.AddCommand<AlertingListCommand>("list").WithDescription("List all alerting rules.");
a.AddCommand<AlertingGetCommand>("get").WithDescription("Show one alerting rule.");
a.AddCommand<AlertingCreateCommand>("create").WithDescription("Create an alerting rule (Admin only).");
Expand Down
16 changes: 16 additions & 0 deletions src/NodePilot.Cli/Commands/Alerting/AlertingCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ protected override async Task<int> RunAsync(CommandContext _, GlobalSettings set
}
}

// Rules are authored as JSON (`np alerting create --file`), so the vocabulary that JSON may use —
// event types, filter-referenceable fields, the channels this installation can deliver on — has to
// be readable from the CLI too. Otherwise the only way to learn a field name is the web UI.
[SupportedOSPlatform("windows")]
public sealed class AlertingCatalogCommand : BaseCommand<GlobalSettings>
{
public AlertingCatalogCommand(SessionResolver s, ApiClientFactory f) : base(s, f) { }
protected override async Task<int> RunAsync(CommandContext _, GlobalSettings settings, SessionContext session, OutputWriter writer, CancellationToken ct)
{
var api = ClientFactory.Create(session);
var catalog = await api.GetAlertingCatalogAsync(ct);
writer.WriteData(catalog, (console, c) => Renderers.AlertingCatalog(console, c));
return ExitCodes.Success;
}
}

public sealed class AlertingIdSettings : GlobalSettings
{
[CommandArgument(0, "<RULE-ID>")]
Expand Down
28 changes: 20 additions & 8 deletions src/NodePilot.Cli/Commands/WorkflowResolver.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System.Net;
using System.Runtime.Versioning;
using NodePilot.Cli.Api;
using NodePilot.Cli.Api.Dtos;
using NodePilot.Core.Clients;

namespace NodePilot.Cli.Commands;

/// <summary>
/// Resolves a CLI workflow argument that may be either a Guid or a name. List endpoint
/// is the only way to look up by name today (no GET /by-name route on the API).
/// Resolves a CLI workflow argument that may be either a Guid or a name. The name half goes
/// through <c>GET /api/workflows/by-name/{name}</c>, so `np` resolves exactly like the engine,
/// the API and the trigger path do: exact case wins, otherwise case-insensitive, ambiguity is
/// an error. Listing and filtering client-side used to disagree with all three — two workflows
/// differing only in case were "ambiguous" to `np` while every other caller picked the exact
/// match — and it dragged the whole workflow list over the wire to resolve one name.
/// </summary>
[SupportedOSPlatform("windows")]
public static class WorkflowResolver
Expand All @@ -16,12 +22,18 @@ public static async Task<WorkflowResponse> ResolveAsync(NodePilotApiClient api,
if (Guid.TryParse(idOrName, out var id))
return await api.GetWorkflowAsync(id, ct);

var all = await api.ListWorkflowsAsync(ct);
var matches = all.Where(w => string.Equals(w.Name, idOrName, StringComparison.OrdinalIgnoreCase)).ToList();
if (matches.Count == 0)
try
{
return await api.GetWorkflowByNameAsync(idOrName, ct);
}
catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
throw new InvalidOperationException($"No workflow named '{idOrName}'.");
if (matches.Count > 1)
throw new InvalidOperationException($"Multiple workflows named '{idOrName}' — disambiguate with the GUID.");
return matches[0];
}
catch (ApiException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
{
throw new InvalidOperationException(
$"Multiple workflows named '{idOrName}' — disambiguate with the GUID.");
}
}
}
22 changes: 22 additions & 0 deletions src/NodePilot.Cli/Output/Renderers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,28 @@ public static void AlertingRules(IAnsiConsole console, IReadOnlyList<Notificatio
console.Write(table);
}

// The event types are already the enum values of the `eventType` field, so one table over the
// fields plus two summary lines says everything without printing the vocabulary twice.
public static void AlertingCatalog(IAnsiConsole console, AlertingCatalogResponse catalog)
{
var table = new Table().Border(TableBorder.Rounded)
.AddColumn("Field")
.AddColumn("Applies")
.AddColumn("Type")
.AddColumn("Values");
foreach (var f in catalog.EventFields)
{
table.AddRow(
Markup.Escape(f.Name),
Markup.Escape(f.Applies),
Markup.Escape(f.Type),
f.Values is { Count: > 0 } v ? Markup.Escape(string.Join(", ", v)) : "[dim]-[/]");
}
console.Write(table);
console.MarkupLine($"Channels: {Markup.Escape(string.Join(", ", catalog.Channels))}");
console.MarkupLine($"Scopeable events: {Markup.Escape(string.Join(", ", catalog.EventTypes.Where(e => e.Scopeable).Select(e => e.Name)))}");
}

public static void AlertingDeliveries(IAnsiConsole console, IReadOnlyList<NotificationDeliveryDto> rows)
{
var table = new Table().Border(TableBorder.Rounded)
Expand Down
12 changes: 12 additions & 0 deletions src/NodePilot.Mcp/Api/Dtos/AlertingDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ public sealed record TestFireRouteResult(string Channel, string Target, bool Suc

public sealed record TestFireResponse(bool AllSucceeded, List<TestFireRouteResult> Results);

// The rule-authoring vocabulary. An agent writing a filter expression has to know which fields
// exist and which values they take; guessing produces rules that never match.
public sealed record AlertingCatalogFieldDto(string Name, string Applies, string Type, IReadOnlyList<string>? Values = null);

public sealed record AlertingCatalogEventTypeDto(string Name, string Category, bool Scopeable);

public sealed record AlertingCatalogResponse(
IReadOnlyList<AlertingCatalogEventTypeDto> EventTypes,
IReadOnlyList<AlertingCatalogFieldDto> EventFields,
IReadOnlyList<string> Channels,
IReadOnlyList<string> DedupTemplateFields);

public sealed record NotificationDeliveryDto(
Guid Id, Guid RuleId, string? RuleName, Guid RouteId, string? Channel, string? Target,
string EventKey, string Status, int Attempt, DateTime CreatedAt, DateTime? SentAt,
Expand Down
7 changes: 7 additions & 0 deletions src/NodePilot.Mcp/Api/NodePilotApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,13 @@ public async Task<List<NotificationRuleResponse>> ListAlertingRulesAsync(Cancell
return await ParseAsync<List<NotificationRuleResponse>>(res, ct);
}

public async Task<AlertingCatalogResponse> GetAlertingCatalogAsync(CancellationToken ct)
{
EnsureReady();
using var res = await _http.GetAsync("api/alerting/catalog", ct);
return await ParseAsync<AlertingCatalogResponse>(res, ct);
}

public async Task<NotificationRuleResponse> GetAlertingRuleAsync(Guid id, CancellationToken ct)
{
EnsureReady();
Expand Down
2 changes: 1 addition & 1 deletion src/NodePilot.Mcp/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Gilt für `src/NodePilot.Mcp/`. Projektweite Regeln stehen in der Root-`CLAUDE.md`.

Reiner HTTP-Client gegen die REST-API (wie die CLI) + In-Proc-Analyse gegen `NodePilot.Core` — **kein** neuer Backend-Pfad (99 Tools, 3 Resources), Transport stdio; reused die DPAPI-Session der CLI (`np auth login`). Destruktive Tools (`delete_*`, `force_unlock_workflow`, `cancel_all_executions`, `test_step`) werden nur bei `NODEPILOT_MCP_ALLOW_DESTRUCTIVE=true` registriert; Workflow-Definitionen werden vor Tool-Output secret-redigiert, bei publish/patch werden echte Secrets aus der gespeicherten Version wiederhergestellt. Volle Doku: `docs/mcp-server.md`.
Reiner HTTP-Client gegen die REST-API (wie die CLI) + In-Proc-Analyse gegen `NodePilot.Core` — **kein** neuer Backend-Pfad (100 Tools, 3 Resources), Transport stdio; reused die DPAPI-Session der CLI (`np auth login`). Destruktive Tools (`delete_*`, `force_unlock_workflow`, `cancel_all_executions`, `test_step`) werden nur bei `NODEPILOT_MCP_ALLOW_DESTRUCTIVE=true` registriert; Workflow-Definitionen werden vor Tool-Output secret-redigiert, bei publish/patch werden echte Secrets aus der gespeicherten Version wiederhergestellt. Volle Doku: `docs/mcp-server.md`.

**Architektur-Konvention:** Neuer API-Endpoint → Methode in `Api/NodePilotApiClient.cs` (DTOs in `Api/Dtos/` dupliziert) + `[McpServerTool]`-Methode in der passenden `Tools/*Tools.cs` (destruktiv → `DestructiveTools` + `get_safety_status`-Liste pflegen), ggf. Klasse in `Program.cs` via `WithTools<T>()` registrieren (**nie** `WithToolsFromAssembly`), WireMock-Test ergänzen. Frontend-Databus-/Lint-Logik wird in `Analysis/` gespiegelt (`upstreamVariables.ts`, `activityConfigFacts.ts`, `workflowLint.ts`).

Expand Down
14 changes: 14 additions & 0 deletions src/NodePilot.Mcp/Tools/AlertingTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ public sealed class AlertingTools

public AlertingTools(NodePilotApiClient api) => _api = api;

[McpServerTool(Name = "get_alerting_catalog", ReadOnly = true)]
[Description("Get the alerting rule vocabulary: supported event types (with category and whether they can be scoped), the fields a filter expression may reference (with type and allowed values), the notification channels this installation can deliver on, and the fields usable in a dedup-key template. Read this before authoring a filterExpressionJson — a filter over an unknown field never matches.")]
public async Task<object> GetAlertingCatalog(CancellationToken cancellationToken = default)
{
var catalog = await ApiErrorMapper.Guard(() => _api.GetAlertingCatalogAsync(cancellationToken));
return new
{
eventTypes = catalog.EventTypes,
eventFields = catalog.EventFields,
channels = catalog.Channels,
dedupTemplateFields = catalog.DedupTemplateFields,
};
}

[McpServerTool(Name = "list_alerting_rules", ReadOnly = true)]
[Description("List alerting rules with their event types, scope, throttle and route channels.")]
public async Task<object> ListAlertingRules(CancellationToken cancellationToken = default)
Expand Down
1 change: 1 addition & 0 deletions src/nodepilot-docs-ui/content/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ np observability query-range --query "rate(nodepilot_workflows_total[5m])" \

```bash
# Alerting (Read Admin/Op; Create/Edit/Delete/Test-Fire Admin-only)
np alerting catalog # Event-Typen, Filter-Felder, Kanäle
np alerting list
np alerting get 9a2f...
np alerting create --file ./rule.json
Expand Down
Loading
Loading