Writing a Plugin
Plugin Configuration covers how to declare a plugin in .mcpd.toml. This page covers how to build one.
A plugin is a standalone executable that mcpd launches as a child process and communicates with over gRPC on a Unix socket. It is not a shared library and it is not loaded into the daemon, so it can be written in any language with gRPC support.
Protocol and SDKs
The canonical protocol definition is mcpd-proto (plugins/v1/plugin.proto, package mozilla.mcpd.plugins.v1). Everything below is generated from or implements that service.
Four SDKs wrap the generated stubs and the startup handshake:
The Python, Rust, and .NET repositories each include an examples/ directory with runnable plugins.
For background, see mcpd plugins: extend your agent infrastructure without touching your code.
Startup Handshake
mcpd owns the process lifecycle. It executes the plugin binary with two flags:
/path/to/plugins/jwt-auth --address /tmp/plugin-jwt-auth-1.sock --network unixThe plugin is responsible for creating and listening on that socket. mcpd polls the address until it accepts a connection, then dials it and, in order, calls:
1
Configure
Deliver plugin configuration
2
CheckReady
Confirm the plugin can serve requests
3
GetMetadata
Read the plugin's reported name, version and commit hash
GetCapabilities is not part of startup: mcpd calls it lazily the first time a configured flow needs checking, then caches the result. A plugin must still implement it.
If the socket does not accept a connection within the start timeout (10 seconds by default), the process is killed and the plugin fails to load. Listen on the socket before doing any slow initialisation work.
Plugin stdout and stderr are captured and forwarded into the daemon's logs, with levels inferred from the output, so a plugin should log to stderr rather than trying to manage its own log files.
Version Pinning with commit_hash
GetMetadata returns a commit_hash that the plugin reports about itself. If the corresponding entry in .mcpd.toml sets commit_hash, the two must match:
A mismatch fails the plugin with:
If commit_hash is omitted from the configuration, whatever the plugin reports is accepted. The check exists so a deployment can pin the exact plugin build it expects, so it is only as strong as the plugin's honesty about its own build — treat it as a deployment guard, not a security control.
Handling Requests
Two RPCs do the actual work. A plugin reports the flows it supports through GetCapabilities; the flows setting in .mcpd.toml selects which of those it is actually run for:
HandleRequestruns during the request phase, before the call reaches the MCP server.HandleResponseruns during the response phase, on every upstream response regardless of status.
Both return a response carrying a Continue flag. From HandleRequest, Continue=false rejects the request before it reaches the MCP server and returns the plugin's response to the client. From HandleResponse the upstream call has already happened, so Continue=false stops the remaining response plugins and returns that response rather than rejecting the original request. See Required Plugins for how rejections and failures differ depending on whether the plugin is marked required.
Category Constraints
The category a plugin is configured under is not just ordering metadata; it changes what the plugin is permitted to do.
content
The only category that may mutate a request or response
observability
Runs in parallel with other observability plugins; may not mutate
all others
May observe or reject, but not mutate
A plugin in a non-content category that returns modified content will not have those modifications applied. If your plugin needs to rewrite payloads, it belongs in content.
Because observability plugins run concurrently, an optional observability plugin's rejection is ignored rather than stopping the pipeline. See Observability Plugin Execution.
Health and Shutdown
CheckHealth and CheckReady are separate calls, in the Kubernetes sense: readiness gates whether the plugin is brought into service at startup, health reports ongoing liveness.
On shutdown mcpd calls Stop, closes the gRPC connection, and waits for the process to exit. A plugin that does not exit within the force-kill timeout (2 seconds) is killed, and the daemon allows 5 seconds for the graceful Stop call itself. Long-running flush or export work in a plugin's shutdown path should be bounded accordingly, or it will be cut short.
Request and response RPCs are each subject to a per-call timeout (5 seconds by default), so a plugin that blocks on a slow external dependency will fail the call rather than stall the pipeline.
Deployment
The plugin binary must be placed in the directory named by [plugins].dir and be executable. If mcpd runs in a container, the plugin must be built for the container's platform, not the host's. See Plugin Directory and Architecture and libc compatibility.
Last updated