Skip to content

Commit aeb2d67

Browse files
authored
Add plugin subsystem for HTTP request/response processing (#209)
* Add plugin category properties and ordering * Add types describing an 'instance' of a plugin * Support passing in plugin config to the daemon as an option * Add pipeline that handles request processing with configured, running plugins * Provide the pipeline as middleware * Add gRPC adapter for interacting with plugins * Add plugin manager to handle the lifecycle of a plugin * Update the NOTICE file
1 parent cad312d commit aeb2d67

22 files changed

+3776
-65
lines changed

NOTICE

Lines changed: 728 additions & 0 deletions
Large diffs are not rendered by default.

cmd/daemon.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
414414
return fmt.Errorf("error creating daemon options: %w", err)
415415
}
416416

417+
// Add plugin configuration if present.
418+
if cfg.Plugins != nil {
419+
opts = append(opts, daemon.WithPluginConfig(cfg.Plugins))
420+
}
421+
417422
d, err := daemon.NewDaemon(deps, opts...)
418423
if err != nil {
419424
return fmt.Errorf("failed to create mcpd daemon instance: %w", err)

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/mozilla-ai/mcpd/v2
22

3-
go 1.25.0
3+
go 1.25.1
44

55
require (
66
github.com/BurntSushi/toml v1.5.0
@@ -9,11 +9,14 @@ require (
99
github.com/go-chi/cors v1.2.2
1010
github.com/hashicorp/go-hclog v1.6.3
1111
github.com/mark3labs/mcp-go v0.41.1
12+
github.com/mozilla-ai/mcpd-plugins-sdk-go v0.0.2
1213
github.com/spf13/cobra v1.10.1
1314
github.com/spf13/pflag v1.0.10
1415
github.com/stretchr/testify v1.11.1
1516
github.com/xeipuuv/gojsonschema v1.2.0
1617
golang.org/x/sync v0.17.0
18+
google.golang.org/grpc v1.76.0
19+
google.golang.org/protobuf v1.36.10
1720
gopkg.in/yaml.v3 v3.0.1
1821
)
1922

@@ -37,6 +40,9 @@ require (
3740
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
3841
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
3942
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
43+
golang.org/x/net v0.44.0 // indirect
4044
golang.org/x/sys v0.36.0 // indirect
45+
golang.org/x/text v0.29.0 // indirect
46+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
4147
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
4248
)

go.sum

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
2121
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
2222
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
2323
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
24+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
25+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
26+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
27+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
28+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
29+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
2430
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2531
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2632
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -50,6 +56,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
5056
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
5157
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
5258
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
59+
github.com/mozilla-ai/mcpd-plugins-sdk-go v0.0.2 h1:G4/vU3KzFuwZUjA438vkK65phljk0YrDZCm1NQWVyTI=
60+
github.com/mozilla-ai/mcpd-plugins-sdk-go v0.0.2/go.mod h1:hIW669XO96LwfiAiX5C0qK+vmPXaNhCKRH553ACQ/F4=
5361
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5462
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5563
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -79,6 +87,20 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
7987
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
8088
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
8189
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
90+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
91+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
92+
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
93+
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
94+
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
95+
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
96+
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
97+
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
98+
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
99+
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
100+
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
101+
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
102+
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
103+
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
82104
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
83105
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
84106
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -89,6 +111,16 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
89111
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
90112
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
91113
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
114+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
115+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
116+
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
117+
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
118+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
119+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
120+
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
121+
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
122+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
123+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
92124
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
93125
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
94126
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

internal/config/config.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (c *Config) Plugin(category string, name string) (PluginEntry, bool) {
134134
if c.Plugins == nil {
135135
return PluginEntry{}, false
136136
}
137-
return c.Plugins.plugin(category, name)
137+
return c.Plugins.plugin(Category(category), name)
138138
}
139139

140140
// UpsertPlugin creates or updates a plugin entry and saves the configuration.
@@ -143,7 +143,7 @@ func (c *Config) UpsertPlugin(category string, entry PluginEntry) (context.Upser
143143
c.Plugins = &PluginConfig{}
144144
}
145145

146-
result, err := c.Plugins.upsertPlugin(category, entry)
146+
result, err := c.Plugins.upsertPlugin(Category(category), entry)
147147
if err != nil {
148148
return result, err
149149
}
@@ -169,7 +169,7 @@ func (c *Config) DeletePlugin(category string, name string) (context.UpsertResul
169169
return context.Noop, fmt.Errorf("no plugins configured")
170170
}
171171

172-
result, err := c.Plugins.deletePlugin(category, name)
172+
result, err := c.Plugins.deletePlugin(Category(category), name)
173173
if err != nil {
174174
return result, err
175175
}
@@ -194,7 +194,7 @@ func (c *Config) ListPlugins(category string) []PluginEntry {
194194
if c.Plugins == nil {
195195
return nil
196196
}
197-
return c.Plugins.listPlugins(category)
197+
return c.Plugins.listPlugins(Category(category))
198198
}
199199

200200
// keyFor generates a temporary version of the ServerEntry to be used as a composite key.

internal/config/config_test.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func TestUpsertPlugin_CreatesAndPersists(t *testing.T) {
352352
Flows: []Flow{FlowRequest, FlowResponse},
353353
}
354354

355-
result, err := cfg.UpsertPlugin(CategoryAuthentication, entry)
355+
result, err := cfg.UpsertPlugin(CategoryAuthentication.String(), entry)
356356
require.NoError(t, err)
357357
require.Equal(t, "created", string(result))
358358

@@ -394,7 +394,7 @@ func TestUpsertPlugin_UpdatesExisting(t *testing.T) {
394394
Flows: []Flow{FlowRequest, FlowResponse},
395395
}
396396

397-
result, err := cfg.UpsertPlugin(CategoryAuthentication, entry)
397+
result, err := cfg.UpsertPlugin(CategoryAuthentication.String(), entry)
398398
require.NoError(t, err)
399399
require.Equal(t, "updated", string(result))
400400

@@ -426,7 +426,7 @@ func TestDeletePlugin_RemovesAndPersists(t *testing.T) {
426426

427427
require.NoError(t, cfg.saveConfig())
428428

429-
result, err := cfg.DeletePlugin(CategoryAuthentication, "jwt-auth")
429+
result, err := cfg.DeletePlugin(CategoryAuthentication.String(), "jwt-auth")
430430
require.NoError(t, err)
431431
require.Equal(t, "deleted", string(result))
432432

@@ -473,14 +473,14 @@ flows = ["request"]
473473
require.True(t, ok, "Config should support plugin operations")
474474

475475
// Verify plugins loaded.
476-
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication)
476+
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication.String())
477477
require.Len(t, authPlugins, 1)
478478
require.Equal(t, "jwt-auth", authPlugins[0].Name)
479479
require.Equal(t, "abc123", *authPlugins[0].CommitHash)
480480
require.True(t, *authPlugins[0].Required)
481481
require.Equal(t, []Flow{FlowRequest, FlowResponse}, authPlugins[0].Flows)
482482

483-
obsPlugins := pluginCfg.ListPlugins(CategoryObservability)
483+
obsPlugins := pluginCfg.ListPlugins(CategoryObservability.String())
484484
require.Len(t, obsPlugins, 1)
485485
require.Equal(t, "metrics", obsPlugins[0].Name)
486486
require.Equal(t, []Flow{FlowRequest}, obsPlugins[0].Flows)
@@ -524,7 +524,7 @@ func TestLoad_StaticTestdata_BasicPlugins(t *testing.T) {
524524
pluginCfg, ok := cfg.(*Config)
525525
require.True(t, ok)
526526

527-
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication)
527+
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication.String())
528528
require.Len(t, authPlugins, 1)
529529
require.Equal(t, "jwt-auth", authPlugins[0].Name)
530530
require.NotNil(t, authPlugins[0].CommitHash)
@@ -548,26 +548,26 @@ func TestLoad_StaticTestdata_MultiplePlugins(t *testing.T) {
548548
pluginCfg, ok := cfg.(*Config)
549549
require.True(t, ok)
550550

551-
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication)
551+
authPlugins := pluginCfg.ListPlugins(CategoryAuthentication.String())
552552
require.Len(t, authPlugins, 2)
553553
require.Equal(t, "jwt-auth", authPlugins[0].Name)
554554
require.Equal(t, "api-key-auth", authPlugins[1].Name)
555555

556-
authzPlugins := pluginCfg.ListPlugins(CategoryAuthorization)
556+
authzPlugins := pluginCfg.ListPlugins(CategoryAuthorization.String())
557557
require.Len(t, authzPlugins, 1)
558558
require.Equal(t, "rbac", authzPlugins[0].Name)
559559
require.True(t, *authzPlugins[0].Required)
560560

561-
rateLimitPlugins := pluginCfg.ListPlugins(CategoryRateLimiting)
561+
rateLimitPlugins := pluginCfg.ListPlugins(CategoryRateLimiting.String())
562562
require.Len(t, rateLimitPlugins, 1)
563563
require.Equal(t, "token-bucket", rateLimitPlugins[0].Name)
564564

565-
obsPlugins := pluginCfg.ListPlugins(CategoryObservability)
565+
obsPlugins := pluginCfg.ListPlugins(CategoryObservability.String())
566566
require.Len(t, obsPlugins, 1)
567567
require.Equal(t, "metrics", obsPlugins[0].Name)
568568
require.Equal(t, []Flow{FlowRequest, FlowResponse}, obsPlugins[0].Flows)
569569

570-
auditPlugins := pluginCfg.ListPlugins(CategoryAudit)
570+
auditPlugins := pluginCfg.ListPlugins(CategoryAudit.String())
571571
require.Len(t, auditPlugins, 1)
572572
require.Equal(t, "compliance-logger", auditPlugins[0].Name)
573573
require.Equal(t, []Flow{FlowResponse}, auditPlugins[0].Flows)
@@ -587,13 +587,13 @@ func TestLoad_StaticTestdata_MinimalPlugins(t *testing.T) {
587587
pluginCfg, ok := cfg.(*Config)
588588
require.True(t, ok)
589589

590-
require.Len(t, pluginCfg.ListPlugins(CategoryAuthentication), 0)
591-
require.Len(t, pluginCfg.ListPlugins(CategoryAuthorization), 0)
592-
require.Len(t, pluginCfg.ListPlugins(CategoryRateLimiting), 0)
593-
require.Len(t, pluginCfg.ListPlugins(CategoryValidation), 0)
594-
require.Len(t, pluginCfg.ListPlugins(CategoryContent), 0)
595-
require.Len(t, pluginCfg.ListPlugins(CategoryObservability), 0)
596-
require.Len(t, pluginCfg.ListPlugins(CategoryAudit), 0)
590+
require.Len(t, pluginCfg.ListPlugins(CategoryAuthentication.String()), 0)
591+
require.Len(t, pluginCfg.ListPlugins(CategoryAuthorization.String()), 0)
592+
require.Len(t, pluginCfg.ListPlugins(CategoryRateLimiting.String()), 0)
593+
require.Len(t, pluginCfg.ListPlugins(CategoryValidation.String()), 0)
594+
require.Len(t, pluginCfg.ListPlugins(CategoryContent.String()), 0)
595+
require.Len(t, pluginCfg.ListPlugins(CategoryObservability.String()), 0)
596+
require.Len(t, pluginCfg.ListPlugins(CategoryAudit.String()), 0)
597597
}
598598

599599
func TestLoad_StaticTestdata_InvalidPlugins(t *testing.T) {
@@ -663,19 +663,19 @@ func TestLoadSaveRoundTrip_Plugins(t *testing.T) {
663663
reloadedConfig, ok := reloaded.(*Config)
664664
require.True(t, ok)
665665

666-
authPlugins := reloadedConfig.ListPlugins(CategoryAuthentication)
666+
authPlugins := reloadedConfig.ListPlugins(CategoryAuthentication.String())
667667
require.Len(t, authPlugins, 2)
668668

669-
authzPlugins := reloadedConfig.ListPlugins(CategoryAuthorization)
669+
authzPlugins := reloadedConfig.ListPlugins(CategoryAuthorization.String())
670670
require.Len(t, authzPlugins, 1)
671671

672-
rateLimitPlugins := reloadedConfig.ListPlugins(CategoryRateLimiting)
672+
rateLimitPlugins := reloadedConfig.ListPlugins(CategoryRateLimiting.String())
673673
require.Len(t, rateLimitPlugins, 1)
674674

675-
obsPlugins := reloadedConfig.ListPlugins(CategoryObservability)
675+
obsPlugins := reloadedConfig.ListPlugins(CategoryObservability.String())
676676
require.Len(t, obsPlugins, 1)
677677

678-
auditPlugins := reloadedConfig.ListPlugins(CategoryAudit)
678+
auditPlugins := reloadedConfig.ListPlugins(CategoryAudit.String())
679679
require.Len(t, auditPlugins, 1)
680680

681681
require.Equal(t, "jwt-auth", authPlugins[0].Name)

0 commit comments

Comments
 (0)