How to Create a New Plugin
Relevant source files
The following files were used as context for generating this wiki page:
- internal/server-plugin/domain/server-plugin.go
- internal/server-plugin/application/registry.go
- internal/server-plugins/core/plugin.go
- internal/server-plugins/app/plugin.go
- internal/server-plugin/infrastructure/plugin_discovery.go
- internal/server-plugins/onboarding/plugin.go
- internal/server-plugins/domain/plugin.go
- CONTRIBUTING.md
Introduction
The Dokku MCP server features a modular, plugin-based architecture where each plugin encapsulates a specific set of Dokku features. Creating a new plugin is the standard way to extend the server's capabilities, exposing more of Dokku's functionality to MCP clients. This guide details the process of creating, implementing, and registering a new plugin.
Plugins can provide three types of capabilities: Resources (for reading data, like a list of apps), Tools (for performing actions, like creating an app), and Prompts (for generating contextual instructions for an LLM). The system follows Domain-Driven Design (DDD) principles, encouraging a clear separation of concerns within each plugin's implementation.
Sources: README.md, internal/server-plugin/domain/server-plugin.go
The ServerPlugin Interfaces
At its core, every plugin must implement the ServerPlugin interface. To provide functionality, a plugin can also implement one or more provider interfaces: ResourceProvider, ToolProvider, and PromptProvider.
Sources: internal/server-plugin/domain/server-plugin.go:9-29
This diagram illustrates the relationship between the base ServerPlugin interface and the capability provider interfaces.
ServerPlugin Interface Methods
All plugins must implement these methods to provide basic metadata and define their dependency on a Dokku plugin.
| Method | Description |
|---|---|
ID() | A unique, lowercase string identifier for the plugin (e.g., "core", "apps"). |
Name() | A human-readable name for the plugin (e.g., "Core Functionality"). |
Description() | A brief description of the plugin's purpose. |
Version() | The version of the plugin (e.g., "0.1.0"). |
DokkuPluginName() | The name of the corresponding Dokku plugin this MCP plugin depends on (e.g., "postgres", "domains"). If empty, the plugin is considered a "core" plugin and is always active. |
Sources: internal/server-plugin/domain/server-plugin.go:11-18, internal/server-plugin/application/registry.go:190-192
Plugin Structure and Implementation
A new plugin should be created in its own directory within internal/server-plugins/. The project follows Domain-Driven Design, so a complex plugin like app is further organized into application, domain, and infrastructure sub-packages.
A typical plugin implementation consists of:
- A struct that holds the plugin's dependencies, such as a logger or application services.
- A constructor function (e.g.,
New...Plugin) that initializes the plugin and its dependencies. - Implementation of the
ServerPlugininterface methods. - Implementation of provider interfaces (
GetResources,GetTools,GetPrompts) to expose capabilities.
// internal/server-plugins/core/plugin.go:21-42
// CoreServerPlugin provides core Dokku functionality and global configuration
type CoreServerPlugin struct {
coreService *application.CoreService
logger *slog.Logger
cfg *config.ServerConfig
}
// NewCoreServerPlugin creates a new core functionality server plugin
func NewCoreServerPlugin(client dokkuApi.DokkuClient, logger *slog.Logger, cfg *config.ServerConfig) serverDomain.ServerPlugin {
// Create infrastructure adapter
adapter := infrastructure.NewDokkuCoreAdapter(client, logger)
// Create application service
coreService := application.NewCoreService(
adapter, // SystemRepository
// ... other repositories
logger,
)
return &CoreServerPlugin{
coreService: coreService,
logger: logger,
cfg: cfg,
}
}
Sources: internal/server-plugins/core/plugin.go:21-42, internal/server-plugins/app/plugin.go:27-40
Providing Capabilities
Plugins expose their functionality to the MCP server by implementing one or more provider interfaces.
Providing Resources
Resources are used for read-only operations that expose data to the LLM. To provide resources, a plugin must implement the ResourceProvider interface.
- Implement
GetResources: This method returns a slice ofdomain.Resourcestructs. - Define
ResourceStructs: Each struct defines a single resource. - Implement Handlers: Each resource needs a handler function that matches the
server.ResourceHandlerFuncsignature.
Resource Field | Description |
|---|---|
URI | The unique URI for the resource (e.g., dokku://core/plugins). |
Name | A human-readable name (e.g., "Dokku Plugins"). |
Description | A description of the data provided by the resource. |
MIMEType | The MIME type of the resource content (e.g., application/json). |
Handler | The function that is called when the resource is requested. |
Sources: internal/server-plugin/domain/server-plugin.go:32-38, internal/server-plugins/core/plugin.go:63-82
The following diagram shows the flow of a resource request.
Providing Tools
Tools are used for performing actions (mutations) on the server, such as creating or scaling an app. To provide tools, a plugin must implement the ToolProvider interface.
- Implement
GetTools: This method returns a slice ofdomain.Toolstructs. - Define
ToolStructs: Each struct defines a single tool. - Implement Builders and Handlers: Each tool needs a
Builderfunction to define its schema for the MCP client and aHandlerfunction to execute its logic.
Tool Field | Description |
|---|---|
Name | The unique name of the tool (e.g., create_app). |
Description | A description of what the tool does. |
Builder | A function that returns an mcp.Tool schema, defining its parameters. |
Handler | The function that is called to execute the tool's logic. |
Sources: internal/server-plugin/domain/server-plugin.go:40-45, internal/server-plugins/app/plugin.go:71-99
This sequence diagram illustrates a typical tool call.
Providing Prompts
Prompts are used to generate contextual instructions for an LLM, often to guide it in complex diagnostic or multi-step tasks. To provide prompts, a plugin must implement the PromptProvider interface.
Prompt Field | Description |
|---|---|
Name | The unique name of the prompt (e.g., app_doctor). |
Description | A description of the prompt's purpose. |
Builder | A function that returns an mcp.Prompt schema, defining its arguments. |
Handler | The function that generates and returns the prompt content. |
Sources: internal/server-plugin/domain/server-plugin.go:47-52, internal/server-plugins/app/plugin.go:101-111
Plugin Registration and Discovery
Plugins are not registered manually. Instead, they are discovered and managed by the DynamicServerPluginRegistry using the uber-go/fx dependency injection framework.
1. Provide the Plugin to the DI Container
In the plugin's package, create an fx.Module that provides your plugin implementation. The plugin must be tagged with the server_plugins group for the registry to discover it.
// internal/server-plugins/app/plugin.go:477-491
var Module = fx.Module("app",
fx.Provide(
// ... provide dependencies like repositories
// Provide the main plugin
fx.Annotate(
NewAppsServerPlugin,
fx.As(new(domain.ServerPlugin)),
fx.ResultTags(`group:"server_plugins"`),
),
),
)
Sources: internal/server-plugins/app/plugin.go:477-491
2. Dynamic Activation Lifecycle
The DynamicServerPluginRegistry periodically synchronizes the state of MCP plugins with the installed and enabled Dokku plugins on the target server.
This flow diagram shows the logic of the syncServerPlugins function, which determines which MCP plugins should be active.
- Core Plugins: If a plugin's
DokkuPluginName()method returns an empty string, it is considered a "core" plugin and will always be active. Thecoreandonboardingplugins are examples of this. - Dependent Plugins: If a plugin returns a name (e.g., "domains"), the registry will activate it only if the
dokku plugin:listcommand shows that the "domains" plugin isenabledon the Dokku server.
Sources: internal/server-plugin/application/registry.go:190-192, internal/server-plugins/core/plugin.go:59, internal/server-plugins/domain/plugin.go:26
Summary
Creating a new plugin involves a few key steps: defining a struct that implements the ServerPlugin interface, exposing functionality through the ResourceProvider, ToolProvider, or PromptProvider interfaces, and registering the plugin with the dependency injection system using an fx.Module. The system's dynamic discovery mechanism will then automatically manage the plugin's lifecycle based on the state of the underlying Dokku installation, ensuring that MCP capabilities accurately reflect the server's features.