diff --git a/appsettings.Development.json b/appsettings.Development.json
index d6f0345..26c189d 100644
--- a/appsettings.Development.json
+++ b/appsettings.Development.json
@@ -27,6 +27,10 @@
"OtlpExporter": "http://localhost:4317/"
},
"CasCap": {
+ "AuditConfig": {
+ "ApprovalEnabled": false,
+ "NeverRequireApproval": [ "UnlockHouseDoor", "SetSmartPlugPower", "SwitchOffAllHouseLights" ]
+ },
"FeatureConfig": {
"EnabledFeatures": "Fronius,Buderus,DoorBird,Knx"
},
diff --git a/appsettings.json b/appsettings.json
index 84a9bac..45653de 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -28,6 +28,15 @@
"OtlpExporter": "http://opentelemetry-collector.monitoring.svc:4317"
},
"CasCap": {
+ "AuditConfig": {
+ "Enabled": true,
+ "RedisStreamKey": "audit:mcp:calls",
+ "MaxStreamLength": 10000,
+ "ApprovalEnabled": true,
+ "ApprovalTimeoutMs": 60000,
+ "AlwaysRequireApproval": [],
+ "NeverRequireApproval": []
+ },
"CachingConfig": {
"RemoteCacheConnectionString": "redis-master.data.svc:6379,allowAdmin=true,abortConnect=false,connectTimeout=1000",
"DistributedLockingEnabled": true,
diff --git a/src/CasCap.App.Server/Program.cs b/src/CasCap.App.Server/Program.cs
index 3253f4c..c4158fc 100644
--- a/src/CasCap.App.Server/Program.cs
+++ b/src/CasCap.App.Server/Program.cs
@@ -32,6 +32,9 @@
// SystemMcpQueryService is referenced by all agents — register unconditionally.
builder.Services.AddSystemMcp();
+ // Register MCP audit middleware and configuration.
+ builder.Services.AddMcpAudit();
+
// Register SignalR services unconditionally so IHubContext<> is always resolvable for
// HausHub sinks discovered during assembly scanning (e.g. HausHubSinkBuderusService).
// The hub endpoint mapping and Redis backplane are configured later when SignalRHub is enabled.
diff --git a/src/CasCap.SmartHaus/Attributes/RequiresApprovalAttribute.cs b/src/CasCap.SmartHaus/Attributes/RequiresApprovalAttribute.cs
new file mode 100644
index 0000000..b5f21c9
--- /dev/null
+++ b/src/CasCap.SmartHaus/Attributes/RequiresApprovalAttribute.cs
@@ -0,0 +1,16 @@
+namespace CasCap.Attributes;
+
+///
+/// Marks an MCP tool method as requiring explicit human approval before execution.
+///
+///
+/// When applied to a method decorated with ,
+/// the will gate execution behind a Signal poll
+/// (or console confirmation in development) before invoking the tool.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class RequiresApprovalAttribute : Attribute
+{
+ /// Human-readable reason displayed in the approval prompt.
+ public string? Reason { get; init; }
+}
diff --git a/src/CasCap.SmartHaus/Extensions/HausServiceCollectionExtensions.cs b/src/CasCap.SmartHaus/Extensions/HausServiceCollectionExtensions.cs
index 7a5cc1b..3b0bba0 100644
--- a/src/CasCap.SmartHaus/Extensions/HausServiceCollectionExtensions.cs
+++ b/src/CasCap.SmartHaus/Extensions/HausServiceCollectionExtensions.cs
@@ -9,6 +9,16 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class HausServiceCollectionExtensions
{
+ ///
+ /// Registers the and singleton
+ /// for MCP tool call auditing and human-in-the-loop approval.
+ ///
+ public static void AddMcpAudit(this IServiceCollection services)
+ {
+ services.AddCasCapConfiguration();
+ services.TryAddSingleton();
+ }
+
///
/// Registers and its configuration dependencies
/// (, ).
@@ -76,8 +86,14 @@ public static void AddComms(this WebApplicationBuilder builder, bool lite = fals
/// Optional root AI configuration supplying shared
/// and .
///
+ ///
+ /// Optional delegate to configure the pipeline (e.g. add middleware).
+ /// When null and the is registered, the audit middleware
+ /// is wired automatically.
+ ///
public static (IChatClient chatClient, AIAgent agent, string instructions) CreateAgent(this WebApplicationBuilder builder,
- ProviderConfig provider, AgentConfig agentConfig, IServiceProvider serviceProvider, List? tools = null, string? otelSourceName = null, AIConfig? aiConfig = null)
+ ProviderConfig provider, AgentConfig agentConfig, IServiceProvider serviceProvider, List? tools = null,
+ string? otelSourceName = null, AIConfig? aiConfig = null, Action? configureAgent = null)
{
// Infrastructure auth (k8s ingress basic auth) is only needed for Ollama
// when running outside the cluster. OpenAI/AzureOpenAI auth is handled separately.
@@ -102,12 +118,26 @@ public static (IChatClient chatClient, AIAgent agent, string instructions) Creat
: null;
return AgentExtensions.CreateAgent(provider, agentConfig, httpClient, tools,
+ configureAgent: configureAgent ?? BuildDefaultAgentMiddleware(serviceProvider),
instructionsAssembly: typeof(HausServiceCollectionExtensions).Assembly,
aiConfig: aiConfig,
otelSourceName: otelSourceName,
tokenCredential: tokenCredential);
}
+ ///
+ /// Builds a default agent middleware pipeline that wires
+ /// when it is registered in the service provider.
+ ///
+ private static Action? BuildDefaultAgentMiddleware(IServiceProvider serviceProvider)
+ {
+ var auditMiddleware = serviceProvider.GetService();
+ if (auditMiddleware is null)
+ return null;
+
+ return b => b.Use(auditMiddleware.InvokeAsync);
+ }
+
///
/// Registers the hub-side event sinks
/// (Console and Metrics) enabled in
diff --git a/src/CasCap.SmartHaus/GlobalUsings.cs b/src/CasCap.SmartHaus/GlobalUsings.cs
index 9c62bdd..ccc2f8a 100644
--- a/src/CasCap.SmartHaus/GlobalUsings.cs
+++ b/src/CasCap.SmartHaus/GlobalUsings.cs
@@ -1,5 +1,6 @@
global using Azure;
global using CasCap.Abstractions;
+global using CasCap.Attributes;
global using CasCap.Common.Abstractions;
global using CasCap.Common.Exceptions;
global using CasCap.Common.Extensions;
diff --git a/src/CasCap.SmartHaus/Models/McpAuditRecord.cs b/src/CasCap.SmartHaus/Models/McpAuditRecord.cs
new file mode 100644
index 0000000..19e23a9
--- /dev/null
+++ b/src/CasCap.SmartHaus/Models/McpAuditRecord.cs
@@ -0,0 +1,46 @@
+namespace CasCap.Models;
+
+///
+/// Immutable audit record emitted for every MCP tool invocation.
+///
+///
+/// Written to structured logs and optionally to a Redis Stream by .
+///
+public sealed record McpAuditRecord
+{
+ /// UTC timestamp when the tool invocation started.
+ [Description("UTC timestamp of the invocation.")]
+ public required DateTime TimestampUtc { get; init; }
+
+ /// The tool name as registered in the MCP server (snake_case).
+ [Description("MCP tool name.")]
+ public required string ToolName { get; init; }
+
+ /// Serialised arguments passed to the tool.
+ [Description("JSON-encoded arguments dictionary.")]
+ public required string Arguments { get; init; }
+
+ /// Duration of the tool execution.
+ [Description("Execution duration.")]
+ public required TimeSpan Duration { get; init; }
+
+ /// Whether the invocation completed successfully.
+ [Description("True if the tool returned without throwing.")]
+ public required bool Success { get; init; }
+
+ /// Exception type name if the invocation failed; otherwise null.
+ [Description("Exception type if failed.")]
+ public string? ErrorType { get; init; }
+
+ /// Exception message if the invocation failed; otherwise null.
+ [Description("Error message if failed.")]
+ public string? ErrorMessage { get; init; }
+
+ /// Whether human approval was required for this invocation.
+ [Description("True if the tool required human approval.")]
+ public bool ApprovalRequired { get; init; }
+
+ /// Whether approval was granted (null if not required).
+ [Description("True if approved, false if denied, null if not required.")]
+ public bool? ApprovalGranted { get; init; }
+}
diff --git a/src/CasCap.SmartHaus/Models/_AuditConfig.cs b/src/CasCap.SmartHaus/Models/_AuditConfig.cs
new file mode 100644
index 0000000..9946835
--- /dev/null
+++ b/src/CasCap.SmartHaus/Models/_AuditConfig.cs
@@ -0,0 +1,56 @@
+namespace CasCap.Models;
+
+///
+/// Configuration for the MCP tool call auditing and human-in-the-loop approval gate.
+///
+///
+/// Bound from the CasCap:AuditConfig section in appsettings.json.
+/// Controls both the audit logging sink and the approval workflow behaviour.
+///
+public record AuditConfig : IAppConfig
+{
+ ///
+ public static string ConfigurationSectionName => $"{nameof(CasCap)}:{nameof(AuditConfig)}";
+
+ /// Whether MCP tool call auditing is enabled.
+ /// Defaults to true.
+ public bool Enabled { get; init; } = true;
+
+ /// Redis Stream key for durable audit history.
+ /// Defaults to "audit:mcp:calls".
+ [Required, MinLength(1)]
+ public string RedisStreamKey { get; init; } = "audit:mcp:calls";
+
+ /// Maximum number of entries retained in the Redis audit stream (MAXLEN).
+ /// Defaults to 10000. Set to 0 to disable trimming.
+ [Range(0, int.MaxValue)]
+ public int MaxStreamLength { get; init; } = 10_000;
+
+ /// Whether the human-in-the-loop approval gate is enabled.
+ /// Defaults to true.
+ public bool ApprovalEnabled { get; init; } = true;
+
+ /// Timeout in milliseconds for waiting for an approval response.
+ /// Defaults to 60000 (60 seconds). Used by .
+ [Range(1000, 300_000)]
+ public int ApprovalTimeoutMs { get; init; } = 60_000;
+
+ ///
+ /// Tool names that always require approval regardless of the .
+ ///
+ ///
+ /// Matching is case-insensitive — use either PascalCase method names (e.g. UnlockHouseDoor)
+ /// or snake_case registry names (e.g. unlock_house_door).
+ /// Config-driven override for operational control without redeployment.
+ ///
+ public string[] AlwaysRequireApproval { get; init; } = [];
+
+ ///
+ /// Tool names that are exempt from approval even if decorated with .
+ ///
+ ///
+ /// Matching is case-insensitive — use either PascalCase method names or snake_case registry names.
+ /// Useful for development environments or trusted automation scenarios.
+ ///
+ public string[] NeverRequireApproval { get; init; } = [];
+}
diff --git a/src/CasCap.SmartHaus/Services/Mcp/FrontDoorMcpQueryService.cs b/src/CasCap.SmartHaus/Services/Mcp/FrontDoorMcpQueryService.cs
index ec51bb1..1cb1a2d 100644
--- a/src/CasCap.SmartHaus/Services/Mcp/FrontDoorMcpQueryService.cs
+++ b/src/CasCap.SmartHaus/Services/Mcp/FrontDoorMcpQueryService.cs
@@ -24,6 +24,7 @@ public partial class FrontDoorMcpQueryService(IDoorBirdQueryService doorBirdQuer
///
[McpServerTool]
[Description("Unlocks the front door by triggering the electric door release.")]
+ [RequiresApproval(Reason = "Physical security action — unlocks the front door.")]
public Task UnlockHouseDoor() => doorBirdQuerySvc.UnlockFrontDoor();
///
diff --git a/src/CasCap.SmartHaus/Services/Mcp/McpAuditMiddleware.cs b/src/CasCap.SmartHaus/Services/Mcp/McpAuditMiddleware.cs
new file mode 100644
index 0000000..f264483
--- /dev/null
+++ b/src/CasCap.SmartHaus/Services/Mcp/McpAuditMiddleware.cs
@@ -0,0 +1,286 @@
+using CasCap.Attributes;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using System.Collections.Frozen;
+using System.Text.Json;
+
+namespace CasCap.Services;
+
+///
+/// Function invocation middleware that audits every MCP tool call and optionally
+/// gates execution behind a human-in-the-loop approval step.
+///
+///
+///
+/// Audit records are emitted via structured logging at
+/// and (when Redis is available) appended to a Redis Stream for durable queryable history.
+///
+///
+/// The approval gate inspects the on the underlying
+/// method and the /
+/// config overrides to determine whether to pause execution and poll the user via Signal.
+///
+///
+public sealed class McpAuditMiddleware(
+ ILogger logger,
+ IOptionsMonitor auditConfig,
+ IPollTracker pollTracker,
+ SignalCliRestClientService signalCliSvc,
+ IOptions signalCliConfig,
+ IOptions commsAgentConfig)
+{
+ ///
+ /// Cached set of tool names (PascalCase method names) that carry .
+ /// Built lazily on first invocation from all classes.
+ ///
+ private FrozenDictionary? _approvalLookup;
+
+ /// JSON serializer options for argument logging.
+ private static readonly JsonSerializerOptions s_jsonOptions = new()
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ ///
+ /// The middleware entry point matching the AIAgentBuilder.Use(...) delegate signature.
+ ///
+ public async ValueTask