Argus MCP includes a composable plugin framework that intercepts MCP requests at well-defined hook points. Plugins run inside the middleware chain -- after recovery and before telemetry -- so every request passes through enabled plugins in priority order.
How Plugins Work
Each plugin extends PluginBase and implements one or more async hooks. The PluginManager runs hooks in priority order (lower number = runs first), enforces per-plugin timeouts, and applies optional conditions.
PluginManager.run_hook("tool_pre_invoke", context)
┌────────────────────────────────────────────────┐
│ For each enabled plugin (sorted by priority) │
│ 1. Evaluate conditions (method, backend) │
│ 2. Copy context (copy-on-write) │
│ 3. Run hook with asyncio.timeout │
│ 4. Apply execution_mode on error │
└────────────────────────────────────────────────┘
Hook Points
Plugins receive context at eight points during request processing:
| Hook | When | Typical Use |
|---|---|---|
tool_pre_invoke | Before a tool call is sent to the backend | Input validation, rate limiting, caching |
tool_post_invoke | After a tool call returns | Output scanning, length guards, response caching |
prompt_pre_fetch | Before a prompt is fetched | Access control, argument sanitization |
prompt_post_fetch | After a prompt returns | Content filtering, PII scrubbing |
resource_pre_fetch | Before a resource is read | URI validation, rate limiting |
resource_post_fetch | After a resource returns | Secret detection, content moderation |
on_load | When the plugin is first initialized | Setup, external connections |
on_unload | When the plugin is torn down | Cleanup, flush buffers |
Execution Modes
Each plugin has an execution_mode that controls what happens on failure:
| Mode | Behavior |
|---|---|
enforce | Plugin failure blocks the request (error returned to client) |
enforce_ignore_error | Plugin failure is logged but the request continues (default) |
permissive | Plugin runs best-effort; errors are silently ignored |
disabled | Plugin is skipped entirely |
Conditions
Plugins can be scoped to specific methods or backends:
plugins:
entries:
- name: rate_limiter
conditions:
methods: ["call_tool"] # Only tool calls
backends: ["expensive-backend"] # Only this backend
Configuration
Plugins are configured under the plugins top-level key:
plugins:
enabled: true
entries:
- name: secrets_detection
enabled: true
execution_mode: enforce
priority: 10
timeout: 5.0
settings:
block: true
- name: rate_limiter
enabled: true
priority: 50
settings:
max_requests: 100
window: 60
Plugin Config Fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Plugin identifier |
enabled | boolean | true | Whether the plugin is active |
execution_mode | string | "enforce_ignore_error" | Error handling mode |
priority | integer | 100 | Execution order (0--10000, lower runs first) |
timeout | float | 30.0 | Per-invocation timeout in seconds (0.1--300.0) |
conditions | object | null | Optional method/backend filters |
settings | object | {} | Plugin-specific configuration |
Top-Level Plugin Config
| Field | Type | Default | Description |
|---|---|---|---|
plugins.enabled | boolean | true | Global plugin system toggle |
plugins.entries | list | [] | List of plugin configurations |
Middleware Chain Position
The PluginMiddleware sits in the middleware chain between Recovery and Telemetry:
Auth → Recovery → PluginMiddleware → Telemetry → Audit → Routing
This means plugins run after crash protection (Recovery catches exceptions from plugins too) and before observability (Telemetry records timing including plugin overhead).
External Plugins
Beyond the built-in plugins, Argus MCP provides seven integration plugins for external policy engines, scanners, and content moderation services.
opa_policy
Evaluates tool requests against an Open Policy Agent instance. The plugin sends the request context to the OPA decision endpoint and blocks non-compliant calls.
| Setting | Type | Default | Description |
|---|---|---|---|
opa_url | string | "http://localhost:8181" | OPA server URL |
- name: opa_policy
execution_mode: enforce
priority: 5
settings:
opa_url: "http://localhost:8181"
cedar_policy
Evaluates requests against an Amazon Cedar policy engine instance.
| Setting | Type | Default | Description |
|---|---|---|---|
cedar_url | string | "http://localhost:8180" | Cedar server URL |
- name: cedar_policy
execution_mode: enforce
priority: 5
settings:
cedar_url: "http://localhost:8180"
clamav
Scans request/response payloads for malware using ClamAV.
| Setting | Type | Default | Description |
|---|---|---|---|
host | string | "127.0.0.1:3310" | ClamAV daemon address |
- name: clamav
execution_mode: enforce
priority: 8
settings:
host: "127.0.0.1:3310"
virustotal
Checks content hashes against the VirusTotal database. The API key should be stored in the encrypted secret store and referenced via the VT_API_KEY environment variable.
| Setting | Type | Default | Description |
|---|---|---|---|
api_key | string | (from VT_API_KEY) | VirusTotal API key |
threshold | integer | 3 | Detection count to trigger blocking |
- name: virustotal
execution_mode: enforce
priority: 8
settings:
threshold: 3
llmguard
Sends prompts to an LLM Guard instance for injection and toxicity detection.
| Setting | Type | Default | Description |
|---|---|---|---|
api_url | string | "http://localhost:8800" | LLM Guard API URL |
threshold | float | 0.5 | Score threshold for blocking |
- name: llmguard
execution_mode: enforce
priority: 12
settings:
api_url: "http://localhost:8800"
threshold: 0.5
content_moderation
Routes content through a cloud moderation service. Supports multiple provider backends.
| Setting | Type | Default | Description |
|---|---|---|---|
provider | string | (required) | One of: openai, azure, aws, granite, watson |
- name: content_moderation
execution_mode: enforce
priority: 12
settings:
provider: "openai"
unified_pdp
Combines multiple policy decision engines (OPA, Cedar, custom) into a single evaluation with configurable combination logic.
| Setting | Type | Default | Description |
|---|---|---|---|
engines | list | (required) | List of engine configurations |
combination_mode | string | "all" | How to combine results: all, any, majority |
- name: unified_pdp
execution_mode: enforce
priority: 5
settings:
engines:
- type: opa
url: "http://localhost:8181"
- type: cedar
url: "http://localhost:8180"
combination_mode: "all"
Writing a Custom Plugin
Create a Python file that extends PluginBase and implement the hooks you need. Place it somewhere importable or register it through the plugin discovery mechanism.
Minimal Example
from argus_mcp.plugins.base import PluginBase
class MyPlugin(PluginBase):
"""Example plugin that logs every tool call."""
name = "my_logger"
async def tool_pre_invoke(self, context: dict) -> dict:
tool_name = context.get("tool_name", "unknown")
self.logger.info("Tool called: %s", tool_name)
return context
async def tool_post_invoke(self, context: dict) -> dict:
self.logger.info("Tool completed: %s", context.get("tool_name"))
return context
Plugin Lifecycle
- on_load -- Called once when the plugin manager initializes the plugin. Use for setup (open connections, load config).
- Hook calls --
tool_pre_invoke,tool_post_invoke, etc. are called for each matching request. Context is copy-on-write: modifications to the dict are isolated to your plugin unless you return the modified context. - on_unload -- Called during shutdown. Close connections and flush buffers here.
Configuration Passthrough
Whatever you put under settings: in config.yaml is available as self.settings inside the plugin:
plugins:
entries:
- name: my_logger
enabled: true
priority: 50
settings:
log_level: "DEBUG"
include_args: true
async def on_load(self, context: dict) -> dict:
level = self.settings.get("log_level", "INFO")
self.logger.setLevel(level)
return context
Custom Plugin Conditions
Use conditions to restrict when the plugin runs:
- name: my_logger
conditions:
methods: ["call_tool"]
backends: ["debug-server"]
The plugin will only execute for call_tool requests routed to debug-server.