# plugins/AGENTS.md

Plugin development guidance for AI coding assistants.

## Directory Structure

```
plugins/
├── config.yaml              # Central plugin configuration (Jinja-enabled)
├── README.md                # Plugin documentation
├── install.yaml             # Installation manifest
├── pii_filter/              # Built-in: PII detection and masking
├── deny_filter/             # Built-in: Denylist blocking
├── regex_filter/            # Built-in: Search/replace
├── resource_filter/         # Built-in: Resource validation
├── argument_normalizer/     # Built-in: Input normalization
└── [many more plugins...]   # 42 plugin directories total
```

## Plugin Framework

The plugin framework is provided by the [`cpex`](https://github.com/contextforge-org/contextforge-plugins-framework) package (ContextForge Plugin Extensions).

### Core Interfaces

```python
from cpex.framework import Plugin, PluginConfig, PluginContext
from cpex.framework import (
    PromptPrehookPayload, PromptPrehookResult,
    ToolPreInvokePayload, ToolPreInvokeResult,
    ResourcePreFetchPayload, ResourcePreFetchResult,
    PluginViolation,
)
```

### Hook Lifecycle

Six production hooks:
- `prompt_pre_fetch` / `prompt_post_fetch` - Before/after prompt rendering
- `tool_pre_invoke` / `tool_post_invoke` - Before/after tool execution
- `resource_pre_fetch` / `resource_post_fetch` - Before/after resource fetch

Plugins execute in priority order (ascending). Lower priority runs first.

### Plugin Modes

- `sequential` - Block on violation (use `on_error: ignore` to block violations but allow errors to pass)
- `transform` - Log violations, continue processing
- `disabled` - Loaded but not executed

## Creating a Plugin

### 1. Bootstrap from Template

```bash
mcpplugins bootstrap --destination plugins/my_plugin --type native
```

### 2. Implement the Plugin Class

```python
# plugins/my_plugin/my_plugin.py
from cpex.framework import Plugin, PluginConfig, PluginContext
from cpex.framework import PromptPrehookPayload, PromptPrehookResult, PluginViolation

class MyPlugin(Plugin):
    async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult:
        # Check for forbidden content
        if payload.args and any("forbidden" in str(v) for v in payload.args.values()):
            return PromptPrehookResult(
                continue_processing=False,
                violation=PluginViolation(
                    reason="Forbidden content",
                    description="Blocked by MyPlugin",
                    code="FORBIDDEN",
                    details={"matched": True},
                ),
            )
        # Allow with optional modification
        return PromptPrehookResult(modified_payload=payload)
```

### 3. Register in config.yaml

```yaml
plugins:
  - name: "MyPlugin"
    kind: "plugins.my_plugin.my_plugin.MyPlugin"
    hooks: ["prompt_pre_fetch"]
    mode: "sequential"
    priority: 100
    config:
      # Plugin-specific configuration
      custom_setting: "value"
```

### 4. Enable in .env

```bash
PLUGINS_ENABLED=true
PLUGINS_CONFIG_FILE=plugins/config.yaml
```

## Configuration Schema

`plugins/config.yaml` structure:

```yaml
plugins: []           # List of plugin configurations
plugin_dirs: []       # Additional plugin directories
plugin_settings:
  plugin_timeout: 30            # Per-call timeout (seconds)
  fail_on_plugin_error: false   # Strict error handling
  enable_plugin_api: true       # Enable plugin management API
```

Plugin entry fields:
- `name` - Unique identifier
- `kind` - Fully-qualified class path (native) or `external` (MCP)
- `hooks` - List of hooks to implement
- `mode` - Execution mode
- `priority` - Execution order (lower = earlier)
- `conditions` - Selective execution filters
- `config` - Plugin-specific settings

## External Plugins (MCP)

External plugins run as separate MCP servers.

```yaml
plugins:
  - name: "ExternalFilter"
    kind: "external"
    priority: 10
    mcp:
      proto: STREAMABLEHTTP    # or STDIO
      url: http://localhost:8000/mcp
      # tls:
      #   ca_bundle: /path/to/ca.crt
```

Required tools on external server:
- `get_plugin_config`
- `prompt_pre_fetch`, `prompt_post_fetch`
- `tool_pre_invoke`, `tool_post_invoke`
- `resource_pre_fetch`, `resource_post_fetch`

## Testing Plugins

### Unit Test

```python
import pytest
from cpex.framework import (
    HookType, PluginConfig, PluginContext, GlobalContext,
    PromptPrehookPayload,
)
from plugins.my_plugin.my_plugin import MyPlugin

@pytest.mark.asyncio
async def test_my_plugin_blocks_forbidden():
    cfg = PluginConfig(
        name="test",
        kind="plugins.my_plugin.my_plugin.MyPlugin",
        hooks=[HookType.PROMPT_PRE_FETCH],
        priority=100,
    )
    plugin = MyPlugin(cfg)
    payload = PromptPrehookPayload(name="test", args={"text": "forbidden content"})
    ctx = PluginContext(global_context=GlobalContext(request_id="t-1"))

    result = await plugin.prompt_pre_fetch(payload, ctx)
    assert result.continue_processing is False
    assert result.violation is not None
```

### Run Tests

```bash
pytest tests/unit/mcpgateway/plugins/
make doctest test
```

## Built-in Plugin Examples

| Plugin | Hooks | Purpose |
|--------|-------|---------|
| `PIIFilterPlugin` | prompt/tool pre/post | Detect and mask PII |
| `DenyListPlugin` | prompt pre | Block denylisted words |
| `SearchReplacePlugin` | prompt/tool pre/post | Regex search/replace |
| `ResourceFilterPlugin` | resource pre/post | Validate URIs, size limits |
| `ArgumentNormalizer` | prompt/tool pre | Normalize inputs |

## Key Files

- `plugins/config.yaml` - Plugin configuration
- Plugin base class, models, manager, and registry are provided by the `cpex` package (see `cpex.framework`)
- Bootstrap templates are provided by the `cpex` package via the `mcpplugins bootstrap` CLI command
