The plugin system has been redesigned to be more flexible and less error-prone. Here's what you need to know:
Plugin Registration Flow:
- PluginManager discovers and loads plugins
- PluginManager unpacks each plugin and calls:
toolRegistry.registerTool()for each tool inplugin.toolsworkflowRegistry.registerWorkflow()for each workflow inplugin.workflows
- Registries handle the actual tool/workflow management
- PluginManager tracks which items came from which plugins for cleanup
Key Points:
- ✅ Registries are plugin-agnostic: They only know about individual tools/workflows
- ✅ PluginManager orchestrates: Handles the packaging/organizational concerns
- ✅ Consistent behavior: Manual and plugin registration use the same core logic
The system now only looks for actual plugin files, not individual workflow classes:
✅ Will be loaded as plugins:
plugin.tsorplugin.js*Plugin.ts(e.g.,ExamplePlugin.ts)*.plugin.ts(e.g.,example.plugin.ts)
❌ Will NOT be loaded as plugins:
ExampleQueryWorkflow.ts(individual workflow classes)ExampleOperationWorkflow.ts(individual workflow classes)*.test.tsor*.spec.ts(test files)- Random TypeScript files
Plugins can export in multiple ways:
// Option 1: Default export
export default {
name: 'my-plugin',
version: '1.0.0',
description: 'My plugin',
workflows: [/* workflows */]
}
// Option 2: Named export
export const plugin = {
name: 'my-plugin',
// ...
}
// Option 3: Factory function (detected but requires manual use)
export function createMyPlugin(deps) {
return { /* plugin */ }
}Plugins can have empty workflows initially:
// This is now VALID ✅
export default {
name: 'my-plugin',
version: '1.0.0',
description: 'Plugin with dependency injection',
workflows: [], // Empty initially
async initialize(registry) {
// Having this method allows empty workflows
console.log('Plugin initialized')
}
}# Plugin discovery paths - only scans these locations
PLUGINS_DISCOVERY_PATHS=./src/plugins,./src/workflows
# Auto-load discovered plugins
PLUGINS_AUTOLOAD=true
# Optional: Only load specific plugins
PLUGINS_ALLOWED_LIST=my-plugin,other-plugin
# Optional: Block specific plugins
PLUGINS_BLOCKED_LIST=old-plugin,broken-plugin- Scan paths for files matching plugin naming patterns
- Import files using absolute paths (no more import errors!)
- Detect exports - tries default, named, and factory patterns
- Validate structure - flexible validation allowing dependency injection
- Register plugins - only registers valid plugin objects
my-project/
├── src/
│ ├── plugins/
│ │ ├── MyPlugin.ts # ✅ Structured plugin
│ │ └── other.plugin.ts # ✅ Alternative naming
│ ├── workflows/
│ │ ├── plugin.ts # ✅ Plugin wrapper for workflows
│ │ ├── QueryWorkflow.ts # ❌ Won't be loaded as plugin
│ │ └── OperationWorkflow.ts # ❌ Won't be loaded as plugin
│ └── tools/
│ └── MyTools.ts # ❌ Won't be loaded as plugin
└── plugins/
└── external-plugin.ts # ✅ External plugin location
// src/plugins/MyPlugin.ts
import { AppPlugin } from '@beyondbetter/bb-mcp-server'
export default: AppPlugin = {
name: 'my-plugin',
version: '1.0.0',
description: 'My structured plugin',
workflows: [
new MyQueryWorkflow({ /* deps */ }),
new MyOperationWorkflow({ /* deps */ })
],
async initialize() {
console.log('Plugin initialized')
}
}// src/workflows/plugin.ts
export default {
name: 'my-workflows',
version: '1.0.0',
description: 'Existing workflows as plugin',
workflows: [], // Empty - uses dependency injection
async initialize() {
// Plugin manager allows this pattern
}
}
export function createWorkflowsPlugin(deps) {
return {
name: 'my-workflows',
workflows: [
new QueryWorkflow(deps),
new OperationWorkflow(deps)
]
}
}// Manual approach - bypass plugin discovery entirely
const registry = new WorkflowRegistry()
registry.register(new MyWorkflow())
registry.register(new AnotherWorkflow())The system now provides better error messages:
# Clear file detection
✅ "Checking plugin file: MyPlugin.ts"
❌ "Skipping non-plugin file: MyWorkflow.ts"
# Export detection
✅ "Found plugin export: default"
❌ "Plugin exports factory functions but no plugin object"
# Validation messages
✅ "Plugin has empty workflows but has initialize method - OK"
❌ "Plugin must provide at least one workflow or initialize method"- Use clear plugin file names (
*Plugin.ts,plugin.ts) - Don't try to load individual workflow classes as plugins
- Use dependency injection patterns for flexibility
- Put structured plugins in
src/plugins/ - Use
plugin.tsfiles to wrap existing workflows - Keep individual workflows separate (they won't be auto-loaded)
- Use factory functions for complex dependency injection
- Use
initialize()method for simple initialization - Consider manual registration for complex setups
- Check logs for file detection: "Checking plugin file: ..."
- Look for validation messages about empty workflows
- Use
PLUGINS_ALLOWED_LISTto test specific plugins
If you have existing code that's not working:
-
Individual Workflows Not Loading?
// Create a plugin wrapper: // src/workflows/plugin.ts export function createWorkflowsPlugin(deps) { return { name: 'my-workflows', workflows: [new ExistingWorkflow(deps)] } }
-
Empty Plugin Validation Errors?
// Add initialize method: export default { workflows: [], async initialize() {} // This allows empty workflows }
-
Import Path Errors? ✅ Fixed automatically - now uses absolute paths
-
Too Many False Positives? Use
PLUGINS_ALLOWED_LISTto be more selective
The plugin system is now:
- Smarter - only loads actual plugin files
- Flexible - supports multiple export and initialization patterns
- Robust - better error handling and path resolution
- Simpler - clearer file naming conventions
- Documented - this guide explains everything!
The complexity has been moved into the plugin system itself, so your plugin code can be simpler and more focused on business logic.