Skip to content

Commit c0e524b

Browse files
authored
Add plugin configuration file support (#204)
* Add plugin configuration structures * Define PluginConfig with 7 categories: authentication, authorization, rate_limiting, validation, content, observability, audit * Add PluginEntry with name, commit hash, required flag, and flows * Implement validation with set semantics for flows * Add CRUD methods for plugin management * Add Plugins field to Config struct * Integrate plugin support into Config * Add Plugin, UpsertPlugin, DeletePlugin, ListPlugins methods to Config * Add plugin validation to Config.validate() * Add tests for plugin configuration * Add unit tests for PluginEntry validation, equality, and flow checks * Add unit tests for PluginConfig CRUD operations * Add integration tests for plugin persistence * Add test for invalid plugin config validation * Add plugin configuration testdata files * Add valid configuration examples: basic_plugins.toml, multiple_plugins.toml, minimal_plugins.toml * Add invalid configuration examples for validation testing * Add tests for loading static plugin configuration files * Add tests for loading valid plugin configurations from testdata * Add tests for loading invalid plugin configurations * Add round-trip test for loading and saving plugin configurations * Add plugin configuration documentation * Document plugin categories and execution order * Explain required plugins and content mutation behavior * Document observability plugin parallel execution * Add complete configuration examples * Update mkdocs navigation * Allow plugin entries to be 'equal' with duplicate flows, and ignoring order
1 parent 4eb0668 commit c0e524b

14 files changed

+1900
-1
lines changed

docs/plugin-configuration.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# Plugin Configuration
2+
3+
## Overview
4+
5+
The `mcpd` daemon supports a plugin subsystem for extending request/response processing.
6+
7+
---
8+
9+
## Plugin Categories
10+
11+
!!! info Plugin execution order
12+
Within each category, plugins execute in the order they appear in the configuration file.
13+
14+
Plugins are organized into categories and execute during specific phases of the request lifecycle.
15+
16+
Categories execute in the order shown below for both request and response phases.
17+
18+
| Order | Category | Purpose | Execution |
19+
|-------|------------------|----------------------------------------------|------------|
20+
| 1 | `observability` | Collect metrics and traces (non-blocking) | Parallel |
21+
| 2 | `authentication` | Validate client identity | Sequential |
22+
| 3 | `authorization` | Verify permissions after authentication | Sequential |
23+
| 4 | `rate_limiting` | Enforce request rate limits | Sequential |
24+
| 5 | `validation` | Check request/response structure and content | Sequential |
25+
| 6 | `content` | Transform request/response payloads | Sequential |
26+
| 7 | `audit` | Log compliance and security events | Sequential |
27+
28+
---
29+
30+
## Plugin Execution Flows
31+
32+
Plugins can execute during one or both flows/phases:
33+
34+
* `request`: Executes during the request phase
35+
* `response`: Executes during the response phase
36+
37+
---
38+
39+
## Configuration Format
40+
41+
```toml
42+
[[servers]]
43+
name = "api-server"
44+
package = "uvx::[email protected]"
45+
tools = ["create", "read", "update", "delete"]
46+
47+
[[plugins.authentication]]
48+
name = "jwt-auth"
49+
commit_hash = "abc123"
50+
required = true
51+
flows = ["request"]
52+
53+
[[plugins.authentication]]
54+
name = "api-key-auth"
55+
flows = ["request", "response"]
56+
57+
[[plugins.authorization]]
58+
name = "rbac"
59+
required = true
60+
flows = ["request"]
61+
62+
[[plugins.observability]]
63+
name = "metrics"
64+
flows = ["request", "response"]
65+
```
66+
67+
---
68+
69+
## Plugin Fields
70+
71+
| Field | Type | Required | Description |
72+
|---------------|---------|----------|------------------------------------------------------|
73+
| `name` | string | Yes | Name of the plugin binary in the plugins directory |
74+
| `commit_hash` | string | No | SHA/hash for validating plugin version |
75+
| `required` | boolean | No | Whether plugin failure should block the request |
76+
| `flows` | array | Yes | Execution phases: ["request"], ["response"], or both |
77+
78+
---
79+
80+
## Execution Order
81+
82+
Plugins execute in the order they appear in the configuration file within their category.
83+
84+
```toml
85+
[[plugins.authentication]]
86+
name = "jwt-auth"
87+
flows = ["request"]
88+
89+
[[plugins.authentication]]
90+
name = "api-key-auth"
91+
flows = ["request"]
92+
```
93+
94+
During the request phase, `jwt-auth` executes first, followed by `api-key-auth`.
95+
96+
---
97+
98+
## Required Plugins
99+
100+
!!! warning "Required Plugin Failures"
101+
If a required (serial) plugin fails or rejects a request/response, the overall request is rejected immediately.
102+
103+
Mark plugins as required when their successful execution is critical:
104+
105+
```toml
106+
[[plugins.authentication]]
107+
name = "jwt-auth"
108+
required = true
109+
flows = ["request"]
110+
```
111+
112+
When `required` is not specified or set to `false`, plugin failures are logged but do not block the request.
113+
114+
---
115+
116+
## Content Mutation
117+
118+
!!! info "Content Plugin Behavior"
119+
Only plugins in the `content` category may mutate requests or responses. Modified content is passed to the next plugin in the chain.
120+
121+
Content plugins modify the request by setting the modified request in their response. Other plugin categories can only observe or reject requests.
122+
123+
### Example Content Plugin Flow
124+
125+
```toml
126+
[[plugins.content]]
127+
name = "encryption"
128+
flows = ["request"]
129+
130+
[[plugins.content]]
131+
name = "compression"
132+
flows = ["request"]
133+
```
134+
135+
The `encryption` plugin processes the request first and may modify it. The modified request is then passed to the `compression` plugin.
136+
137+
---
138+
139+
## Observability Plugin Execution
140+
141+
!!! note "Parallel Execution"
142+
Observability plugins run in *parallel* and cannot modify requests or responses.
143+
144+
Observability plugins are designed for metrics collection, tracing, and monitoring. They execute concurrently for performance.
145+
146+
### Required Observability Plugins
147+
148+
If any observability plugin is marked as `required`, request processing waits for all observability plugins to complete before aggregating results.
149+
If any required observability plugin fails, the request is rejected after all have completed.
150+
151+
```toml
152+
[[plugins.observability]]
153+
name = "metrics"
154+
required = true
155+
flows = ["request", "response"]
156+
157+
[[plugins.observability]]
158+
name = "tracing"
159+
flows = ["request", "response"]
160+
```
161+
162+
In this example, both `metrics` and `tracing` run in parallel, but the request will be rejected if `metrics` fails
163+
(once `metrics` and `tracing` have completed).
164+
165+
---
166+
167+
## Multiple Plugins Per Category
168+
169+
You can configure multiple plugins within the same category. They execute in the order defined:
170+
171+
```toml
172+
[[plugins.authentication]]
173+
name = "jwt-auth"
174+
required = true
175+
flows = ["request"]
176+
177+
[[plugins.authentication]]
178+
name = "api-key-auth"
179+
flows = ["request"]
180+
181+
[[plugins.authentication]]
182+
name = "oauth2"
183+
flows = ["request"]
184+
```
185+
186+
Request processing order: `jwt-auth``api-key-auth``oauth2`
187+
188+
---
189+
190+
## Minimal Configuration
191+
192+
Plugins are optional. A configuration file without plugins is valid:
193+
194+
```toml
195+
[[servers]]
196+
name = "simple-server"
197+
package = "uvx::[email protected]"
198+
tools = ["tool1"]
199+
```
200+
201+
---
202+
203+
## Complete Example
204+
205+
```toml
206+
[[servers]]
207+
name = "production-api"
208+
package = "uvx::[email protected]"
209+
tools = ["create_user", "get_user", "update_user", "delete_user"]
210+
211+
[[plugins.authentication]]
212+
name = "jwt-auth"
213+
commit_hash = "a1b2c3d4"
214+
required = true
215+
flows = ["request"]
216+
217+
[[plugins.authorization]]
218+
name = "rbac"
219+
commit_hash = "e5f6g7h8"
220+
required = true
221+
flows = ["request"]
222+
223+
[[plugins.rate_limiting]]
224+
name = "token-bucket"
225+
flows = ["request"]
226+
227+
[[plugins.validation]]
228+
name = "schema-validator"
229+
required = true
230+
flows = ["request", "response"]
231+
232+
[[plugins.content]]
233+
name = "encryption"
234+
flows = ["request", "response"]
235+
236+
[[plugins.observability]]
237+
name = "prometheus-metrics"
238+
required = true
239+
flows = ["request", "response"]
240+
241+
[[plugins.observability]]
242+
name = "distributed-tracing"
243+
flows = ["request", "response"]
244+
245+
[[plugins.audit]]
246+
name = "compliance-logger"
247+
required = true
248+
flows = ["response"]
249+
```
250+
251+
### Execution Flow
252+
253+
#### Request Phase
254+
255+
1. `jwt-auth` (authentication) - sequential
256+
2. `rbac` (authorization) - sequential
257+
3. `token-bucket` (rate_limiting) - sequential
258+
4. `schema-validator` (validation) - sequential
259+
5. `encryption` (content) - sequential
260+
6. `prometheus-metrics` + `distributed-tracing` (observability) - parallel
261+
262+
#### Response Phase
263+
264+
1. `schema-validator` (validation) - sequential
265+
2. `encryption` (content) - sequential
266+
3. `prometheus-metrics` + `distributed-tracing` (observability) - parallel
267+
4. `compliance-logger` (audit) - sequential
268+
269+
---
270+
271+
## Validation
272+
273+
Plugin configurations are validated when the daemon starts or during hot reload. Common validation errors:
274+
275+
* Empty plugin name
276+
* Missing or empty `flows` array
277+
* Invalid flow values (must be `request` or `response`)
278+
* Duplicate flow values

internal/config/config.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/BurntSushi/toml"
1010

11+
"github.com/mozilla-ai/mcpd/v2/internal/context"
1112
"github.com/mozilla-ai/mcpd/v2/internal/flags"
1213
"github.com/mozilla-ai/mcpd/v2/internal/perms"
1314
)
@@ -128,6 +129,74 @@ func (c *Config) SaveConfig() error {
128129
return c.saveConfig()
129130
}
130131

132+
// Plugin retrieves a plugin by category and name.
133+
func (c *Config) Plugin(category string, name string) (PluginEntry, bool) {
134+
if c.Plugins == nil {
135+
return PluginEntry{}, false
136+
}
137+
return c.Plugins.plugin(category, name)
138+
}
139+
140+
// UpsertPlugin creates or updates a plugin entry and saves the configuration.
141+
func (c *Config) UpsertPlugin(category string, entry PluginEntry) (context.UpsertResult, error) {
142+
if c.Plugins == nil {
143+
c.Plugins = &PluginConfig{}
144+
}
145+
146+
result, err := c.Plugins.upsertPlugin(category, entry)
147+
if err != nil {
148+
return result, err
149+
}
150+
151+
if result == context.Noop {
152+
return result, nil
153+
}
154+
155+
if err := c.validate(); err != nil {
156+
return context.Noop, err
157+
}
158+
159+
if err := c.saveConfig(); err != nil {
160+
return context.Noop, fmt.Errorf("failed to save updated config: %w", err)
161+
}
162+
163+
return result, nil
164+
}
165+
166+
// DeletePlugin removes a plugin entry and saves the configuration.
167+
func (c *Config) DeletePlugin(category string, name string) (context.UpsertResult, error) {
168+
if c.Plugins == nil {
169+
return context.Noop, fmt.Errorf("no plugins configured")
170+
}
171+
172+
result, err := c.Plugins.deletePlugin(category, name)
173+
if err != nil {
174+
return result, err
175+
}
176+
177+
if result == context.Noop {
178+
return result, err
179+
}
180+
181+
if err := c.validate(); err != nil {
182+
return context.Noop, err
183+
}
184+
185+
if err := c.saveConfig(); err != nil {
186+
return context.Noop, fmt.Errorf("failed to save updated config: %w", err)
187+
}
188+
189+
return result, nil
190+
}
191+
192+
// ListPlugins returns all plugins in a category.
193+
func (c *Config) ListPlugins(category string) []PluginEntry {
194+
if c.Plugins == nil {
195+
return nil
196+
}
197+
return c.Plugins.listPlugins(category)
198+
}
199+
131200
// keyFor generates a temporary version of the ServerEntry to be used as a composite key.
132201
// It consists of the name of the server and the package without version information.
133202
func keyFor(entry ServerEntry) serverKey {
@@ -173,7 +242,17 @@ func (c *Config) validate() error {
173242
return err
174243
}
175244

176-
// TODO: Add more sub-validation as we add more parts to the config file.
245+
if c.Daemon != nil {
246+
if err := c.Daemon.Validate(); err != nil {
247+
return fmt.Errorf("daemon configuration error: %w", err)
248+
}
249+
}
250+
251+
if c.Plugins != nil {
252+
if err := c.Plugins.Validate(); err != nil {
253+
return fmt.Errorf("plugin configuration error: %w", err)
254+
}
255+
}
177256

178257
return nil
179258
}

0 commit comments

Comments
 (0)