mcpd Plugins: Extend Your Agent Infrastructure Without Touching Your Code
The new plugin system transforms mcpd from a tool-server manager into an extensible enforcement and transformation layer—where authentication, validation, rate limiting, and custom logic live in one governed pipeline.
In October, we shipped mcpd as a “requirements.txt for agentic systems”, a way to declaratively manage your MCP servers across environments. A few weeks later, we followed up with mcpd-proxy, giving teams a single URL to access all their tools.
But as we talked to more teams, a pattern emerged. They weren’t just asking how to run their MCP servers. They were asking what happens in between the agent’s request and the tool-server’s response.
Questions like:
- “How do I enforce authentication across all tool calls?”
- “Can I add rate limiting without modifying every tool-server?”
- “We need to log every request for compliance. Where does that live?”
The answer, until now, was scattered: custom wrappers around tool-servers, bespoke logic embedded in agents, or entirely separate proxy layers. None of it was governed. None of it was consistent.
That's what the mcpd plugin system solves.
What Are mcpd Plugins?
Plugins let you inject logic into mcpd’s request/response pipeline, without modifying your tool-servers or agent code. They run as external binaries, communicate over gRPC, and execute in a fixed, auditable order.
Think of it this way: mcpd was already the single point where your agents connect to tools. Now it’s also the single point where you can observe, validate, transform, and enforce policy on those connections.
Each plugin belongs to a category, and categories execute in a defined sequence:
This isn't arbitrary. The order reflects how enterprise systems typically need to process requests: identify who's calling, check if they're allowed, ensure they're not exceeding limits, validate the payload, optionally transform it, and log everything.
Why External Binaries?
Plugins run as standalone executables. They're not embedded in mcpd's process. This was a deliberate choice:
- Language flexibility: Write plugins in Python, Go, Rust, .NET… anything that can compile to a binary and speak gRPC.
- Isolation: A misbehaving plugin can’t crash mcpd. It can hang or fail, and mcpd will handle that gracefully.
- Independent deployment: Update a plugin without rebuilding mcpd or your tool-servers.
- Reuse existing code: Have an internal auth library in Go? An observability package in Python? Use them directly.
The protocol is defined in mcpd-proto. If you can compile to a self-contained executable and implement that gRPC interface, you can build a plugin.
Configuration
Plugins are declared in your mcpd.toml by category:
[[servers]]
name = "production-api"
package = "uvx::api-server@2.0.0"
tools = ["create_user", "get_user", "update_user"]
[[plugins.authentication]]
name = "jwt-auth"
commit_hash = "a1b2c3d4"
required = true
flows = ["request"]
[[plugins.authorization]]
name = "rbac"
required = true
flows = ["request"]
[[plugins.rate_limiting]]
name = "token-bucket"
flows = ["request"]
[[plugins.content]]
name = "request-enricher"
flows = ["request", "response"]
[[plugins.observability]]
name = "prometheus-metrics"
required = true
flows = ["request", "response"]
[[plugins.audit]]
name = "compliance-logger"
required = true
flows = ["response"]
A few things to note:
- flows declares when the plugin runs: during the request phase, response phase, or both.
- required = true means if this plugin fails or rejects the request, the entire request fails. Use this for authentication, authorization, and anything compliance-critical.
- commit_hash pins a specific version of the plugin binary. At startup, mcpd asks the plugin to report its metadata (including commit hash) and compares it against this configured value—if they don’t match, mcpd rejects the plugin and won’t proceed. This ensures you’re running exactly the code you expect.
- Ordering within a category follows the order in your config file. If you have three authentication plugins, they run in the sequence you define them.
The Execution Flow
Here's what happens when an agent makes a tool call through mcpd:
Request Phase:

Response Phase:

Observability plugins use a scatter/gather pattern: mcpd fans out requests to all observability plugins in parallel, waits for all of them to complete, then aggregates the results before proceeding. This maximizes throughput while still allowing required observability plugins to gate the request if they fail.
Building a Plugin
We provide SDKs to make plugin development straightforward:
- Python: mcpd-plugins-sdk-python (available on PyPI)
- Go: mcpd-plugins-sdk-go
- Rust: mcpd-plugins-sdk-rust (available on crates.io)
- .NET: mcpd-plugins-sdk-dotnet (available on NuGet)
Here's a minimal Python plugin that adds a custom header to every request:
import asyncio
import sys
from mcpd_plugins import BasePlugin, serve
from mcpd_plugins.v1.plugins.plugin_pb2 import (
FLOW_REQUEST,
Capabilities,
HTTPRequest,
HTTPResponse,
Metadata,
)
from google.protobuf.empty_pb2 import Empty
class HeaderPlugin(BasePlugin):
async def GetMetadata(self, request: Empty, context) -> Metadata:
return Metadata(
name="header-injector",
version="1.0.0",
description="Adds team metadata to all requests"
)
async def GetCapabilities(self, request: Empty, context) -> Capabilities:
return Capabilities(flows=[FLOW_REQUEST])
async def HandleRequest(self, request: HTTPRequest, context) -> HTTPResponse:
response = HTTPResponse(**{"continue": True})
response.modified_request.CopyFrom(request)
response.modified_request.headers["X-Team-ID"] = "platform-eng"
response.modified_request.headers["X-Request-Source"] = "mcpd-plugin"
return response
if __name__ == "__main__":
asyncio.run(serve(HeaderPlugin(), sys.argv))
And an authentication plugin that validates Bearer tokens:
class AuthPlugin(BasePlugin):
def __init__(self):
self.valid_token = os.environ.get("AUTH_TOKEN", "")
async def GetMetadata(self, request: Empty, context) -> Metadata:
return Metadata(
name="bearer-auth",
version="1.0.0",
description="Validates Bearer token authentication"
)
async def GetCapabilities(self, request: Empty, context) -> Capabilities:
return Capabilities(flows=[FLOW_REQUEST])
async def HandleRequest(self, request: HTTPRequest, context) -> HTTPResponse:
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return HTTPResponse(
**{"continue": False},
status_code=401,
body=b'{"error": "Missing or invalid Authorization header"}'
)
token = auth_header[7:] # Strip "Bearer "
if token != self.valid_token:
return HTTPResponse(
**{"continue": False},
status_code=403,
body=b'{"error": "Invalid token"}'
)
return HTTPResponse(**{"continue": True})
The pattern is the same across all SDKs: implement the methods you need, declare your flows, and let mcpd handle the rest.
A Real-World Scenario
Let's say you're a platform team responsible for agent infrastructure across multiple engineering squads. You need:
- Unified logging for all agent tool-calls (for debugging and compliance)
- Automatic request tagging with team metadata
- Rate limiting to prevent runaway agents from overwhelming tool-servers
- Fallback handling when a tool-server returns an error
Without plugins, you'd either build separate wrapper services, embed logic in every agent, or modify each tool-server. All of those approaches create drift and maintenance burden.
With plugins, you deploy:
- An observability plugin that logs request/response pairs to your central pipeline
- A content plugin that injects team metadata into every request
- A rate_limiting plugin that enforces per-team quotas
- An audit plugin that triggers fallback logic on tool errors
None of this requires changes to agent code or tool-servers. The rollout happens once, in mcpd's configuration. Every agent connecting through mcpd immediately gets the new behavior.
What This Means for Platform Teams
Compliance becomes tractable. Authentication, authorization, rate limiting, validation, and audit logging live in one chain with fixed execution order. You can explain exactly what happens to every request.
Security gets a single enforcement point. No implicit trust in tool-servers. Plugins can reject, sanitize, or transform requests before any tool is touched.
Policy applies everywhere. A plugin added to mcpd affects every agent connecting through it. No more duplicating logic across tool-servers or SDKs.
Guardrails become modular. Payload validation, access checks, content filtering—implement them as isolated plugins. Swap them out, update them, or disable them without touching anything else.
What's Next
The plugin system is available now. We're continuing to build out:
- Plugin-specific configuration: Delivering config to plugins via gRPC at startup, including secrets from
secrets.prod.toml - Controlled restarts: Graceful plugin lifecycle management
- Stronger governance: Enhanced tooling around required plugins and category extensions
Try It
The plugin system is live in the latest mcpd release. Start with the plugin configuration docs, grab an SDK, and build something.
We'd love to hear what you're building. What enforcement logic do you need? What transformations would make your agent infrastructure cleaner? Open an issue or reach out—your feedback shapes what we build next.
Links: