This guide explains the bb-mcp-server plugin architecture and the differences between tools, workflows, and plugins. Understanding these concepts is essential for building maintainable and scalable MCP servers.
Important: The plugin system has been refactored for cleaner separation of concerns:
ToolRegistry: Manages individual tools viaregisterTool()WorkflowRegistry: Manages individual workflows viaregisterWorkflow()- No plugin methods: Registries focus only on individual item management
PluginManager: Handles plugin loading, discovery, and orchestration- Unpacks plugins: Calls registry methods for each tool/workflow in the plugin
- Tracks relationships: Maintains which items came from which plugins for cleanup
// Path 1: Direct Registration (Manual)
toolRegistry.registerTool('my_tool', definition, handler)
workflowRegistry.registerWorkflow(new MyWorkflow())
// Path 2: Plugin Registration (Dynamic Loading)
pluginManager.registerPlugin(plugin)
// ↳ Internally calls toolRegistry.registerTool() and workflowRegistry.registerWorkflow()Key Benefits:
- ✅ Consistent behavior: Both paths use same core registration logic
- ✅ Clean separation: Registries don't know about plugins
- ✅ Flexible: Use manual registration OR plugin discovery as needed
- ✅ Maintainable: Single source of truth for tool/workflow management
- Quick Reference
- Tools vs Workflows
- Plugin Architecture
- Creating Simple Tools
- Creating Workflows
- Creating Plugins
- Plugin Discovery
- Configuration
- Best Practices
- Examples
| Component | Purpose | When to Use | Complexity |
|---|---|---|---|
| Simple Tools | Direct API operations | Single API calls, data queries, simple operations | Low |
| Workflows | Multi-step business processes | Complex operations, state management, error handling | Medium |
| Plugins | Bundled functionality | Distributable packages, multiple workflows/tools | High |
Definition: Direct, single-purpose functions that perform specific operations.
Characteristics:
- ✅ Single responsibility
- ✅ Direct API integration
- ✅ Minimal state management
- ✅ Fast execution
- ✅ Simple error handling
Example Use Cases:
- Query customer by ID
- Get order status
- Send notification
- Validate data format
- Execute simple API call
Implementation Pattern:
// Simple tool example
registry.registerTool('get_customer', {
title: 'Get Customer',
description: 'Retrieve customer information by ID',
inputSchema: {
customerId: z.string().describe('Customer ID')
}
}, async (args) => {
const customer = await apiClient.getCustomer(args.customerId)
return { content: [{ type: 'text', text: JSON.stringify(customer) }] }
})Definition: Multi-step, stateful processes that orchestrate complex business operations.
Characteristics:
- ✅ Multiple execution steps
- ✅ State management
- ✅ Error recovery and rollback
- ✅ Audit logging
- ✅ Complex validation
- ✅ Business logic orchestration
Example Use Cases:
- Process customer order (create customer → validate payment → create order → send confirmation)
- Data migration (extract → validate → transform → load)
- Complex analysis workflows
- Multi-system integrations
- Business process automation
Implementation Pattern:
// Workflow example
export class ProcessOrderWorkflow extends WorkflowBase {
readonly name = 'process_order'
readonly description = 'Complete order processing workflow'
async execute(params: OrderParams): Promise<WorkflowResult> {
const steps = []
// Step 1: Validate customer
const customer = await this.validateCustomer(params.customerId)
steps.push({ step: 'validate_customer', success: true })
// Step 2: Process payment
const payment = await this.processPayment(params.paymentInfo)
steps.push({ step: 'process_payment', success: true })
// Step 3: Create order
const order = await this.createOrder(customer, payment)
steps.push({ step: 'create_order', success: true })
return { success: true, data: order, completed_steps: steps }
}
}Definition: Bundled collections of tools and workflows that provide cohesive functionality for specific domains or integrations.
Benefits:
- 🎯 Organization: Group related functionality
- 🔄 Reusability: Share across projects
- 📦 Distribution: Package and distribute functionality
- 🔍 Discovery: Automatic loading and registration
- 🏗️ Architecture: Clean separation of concerns
your-plugin/
├── plugin.json # Plugin manifest
├── main.ts # Main plugin export
├── workflows/ # Workflow implementations
│ ├── QueryWorkflow.ts
│ └── OperationWorkflow.ts
├── tools/ # Tool implementations
│ └── UtilityTools.ts
├── types/ # Type definitions
│ └── plugin.types.ts
└── README.md # Plugin documentation
{
"name": "my-business-plugin",
"version": "1.0.0",
"description": "Business workflows and tools for MyCompany",
"main": "main.ts",
"author": "MyCompany Team",
"license": "MIT",
"mcpServer": {
"minVersion": "1.0.0",
"maxVersion": "2.0.0"
},
"workflows": [
{
"name": "my_query_workflow",
"description": "Query business data",
"category": "query"
},
{
"name": "my_operation_workflow",
"description": "Execute business operations",
"category": "operation"
}
],
"tools": [
"get_business_data",
"create_business_record",
"update_business_record"
],
"tags": ["business", "mycompany", "integration"]
}// tools/MyTools.ts
import { ToolRegistry, z } from '@beyondbetter/bb-mcp-server'
export class MyTools {
constructor(private apiClient: MyApiClient, private logger: Logger) {}
registerWith(toolRegistry: ToolRegistry): void {
this.registerGetDataTool(toolRegistry)
this.registerCreateRecordTool(toolRegistry)
}
private registerGetDataTool(registry: ToolRegistry): void {
registry.registerTool('get_my_data', {
title: 'Get My Data',
description: 'Retrieve data from MyAPI',
category: 'MyCompany',
inputSchema: {
query: z.string().describe('Search query'),
limit: z.number().optional().default(10).describe('Results limit')
}
}, async (args) => {
try {
const data = await this.apiClient.getData(args.query, args.limit)
return {
content: [{
type: 'text',
text: JSON.stringify(data, null, 2)
}]
}
} catch (error) {
return {
content: [{
type: 'text',
text: `Error: ${error.message}`
}],
isError: true
}
}
})
}
}Important: Tools are registered with ToolRegistry using registerTool(), not via plugins:
// Direct tool registration (recommended)
toolRegistry.registerTool('get_my_data', definition, handler)
// Plugin-based tool registration (managed by PluginManager)
pluginManager.registerPlugin(plugin) // → calls toolRegistry.registerTool() internally- Single Responsibility: Each tool should do one thing well
- Clear Naming: Use descriptive, action-oriented names
- Input Validation: Use Zod schemas for type safety
- Error Handling: Provide clear error messages
- Documentation: Include helpful descriptions and examples
- Logging: Log important operations for debugging
// workflows/MyWorkflow.ts
import { WorkflowBase } from '@beyondbetter/bb-mcp-server'
import { z } from 'zod'
export class MyBusinessWorkflow extends WorkflowBase {
readonly name = 'my_business_workflow'
readonly version = '1.0.0'
readonly description = 'Execute complex business process'
readonly category = 'operation' as const
readonly parameterSchema = z.object({
userId: z.string().describe('User ID for authentication'),
businessData: z.object({
type: z.enum(['create', 'update', 'delete']),
payload: z.record(z.unknown())
}).describe('Business operation data'),
options: z.object({
validateOnly: z.boolean().default(false),
notifyUsers: z.boolean().default(true)
}).optional()
})
constructor(
private apiClient: MyApiClient,
private logger: Logger
) {
super()
}
async execute(params: z.infer<typeof this.parameterSchema>): Promise<WorkflowResult> {
const startTime = performance.now()
const steps = []
const failedSteps = []
try {
// Step 1: Validate business data
this.logger.info('Validating business data', { userId: params.userId })
const validation = await this.validateBusinessData(params.businessData)
steps.push({
operation: 'validate_data',
success: true,
duration_ms: performance.now() - startTime,
timestamp: new Date().toISOString()
})
if (params.options?.validateOnly) {
return {
success: true,
data: { validation, mode: 'validate-only' },
completed_steps: steps,
failed_steps: [],
metadata: { executionTime: performance.now() - startTime }
}
}
// Step 2: Execute business operation
this.logger.info('Executing business operation', {
type: params.businessData.type,
userId: params.userId
})
const result = await this.executeBusinessOperation(
params.businessData,
params.userId
)
steps.push({
operation: 'execute_operation',
success: true,
data: { operationType: params.businessData.type },
duration_ms: performance.now() - startTime,
timestamp: new Date().toISOString()
})
// Step 3: Send notifications (if enabled)
if (params.options?.notifyUsers) {
await this.sendNotifications(result, params.userId)
steps.push({
operation: 'send_notifications',
success: true,
duration_ms: performance.now() - startTime,
timestamp: new Date().toISOString()
})
}
return {
success: true,
data: result,
completed_steps: steps,
failed_steps: [],
metadata: {
executionTime: performance.now() - startTime,
operationType: params.businessData.type
}
}
} catch (error) {
const failedStep = {
operation: 'workflow_execution',
error_type: 'system_error' as const,
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
}
failedSteps.push(failedStep)
return {
success: false,
error: {
type: 'system_error' as const,
message: error instanceof Error ? error.message : 'Workflow failed',
details: error instanceof Error ? error.stack : undefined,
code: 'WORKFLOW_ERROR',
stack: error instanceof Error ? error.stack : undefined,
recoverable: true
},
completed_steps: steps,
failed_steps: failedSteps,
metadata: { executionTime: performance.now() - startTime }
}
}
}
private async validateBusinessData(data: any): Promise<any> {
// Business validation logic
return { valid: true, issues: [] }
}
private async executeBusinessOperation(data: any, userId: string): Promise<any> {
// Business operation logic
return await this.apiClient.executeOperation(data, userId)
}
private async sendNotifications(result: any, userId: string): Promise<void> {
// Notification logic
this.logger.info('Notifications sent', { userId, resultId: result.id })
}
}- Step Tracking: Record all execution steps for audit trails
- Error Recovery: Implement rollback mechanisms where appropriate
- State Management: Handle complex state transitions properly
- Validation: Validate inputs at multiple stages
- Logging: Comprehensive logging for troubleshooting
- Testing: Unit test individual workflow methods
- Documentation: Document workflow purpose and step details
// main.ts - Main plugin export
import type { AppPlugin } from '@beyondbetter/bb-mcp-server'
import { MyBusinessWorkflow } from './workflows/MyWorkflow.ts'
import { MyTools } from './tools/MyTools.ts'
// Plugin dependencies interface
export interface MyPluginDependencies {
apiClient: MyApiClient
logger: Logger
}
// Plugin factory function
export function createMyPlugin(dependencies: MyPluginDependencies): AppPlugin {
const { apiClient, logger } = dependencies
// Create workflow instances
const workflows = [
new MyBusinessWorkflow(apiClient, logger)
]
return {
name: 'my-business-plugin',
version: '1.0.0',
description: 'Business workflows and tools for MyCompany',
author: 'MyCompany Team',
license: 'MIT',
workflows,
dependencies: ['@beyondbetter/bb-mcp-server'],
tags: ['business', 'mycompany'],
// ✅ CORRECT signature for initialize method (when needed):
async initialize(
dependencies: AppServerDependencies,
toolRegistry: ToolRegistry,
workflowRegistry: WorkflowRegistry
): Promise<void> {
// PluginManager calls: plugin.initialize(pluginDependencies, toolRegistry, workflowRegistry)
logger.info('MyBusiness plugin initialized')
},
async cleanup(): Promise<void> {
logger.info('MyBusiness plugin cleaned up')
}
}
}
// Default export for plugin discovery
const plugin: AppPlugin = {
name: 'my-business-plugin',
version: '1.0.0',
description: 'Business workflows and tools for MyCompany',
workflows: [], // Populated by factory function
// ✅ CORRECT signature for initialize method (when needed):
async initialize(
dependencies: AppServerDependencies,
toolRegistry: ToolRegistry,
workflowRegistry: WorkflowRegistry
): Promise<void> {
// PluginManager calls: plugin.initialize(pluginDependencies, toolRegistry, workflowRegistry)
console.log('Plugin discovered and initialized')
}
}
export default plugin- Configuration: Set discovery paths in environment variables
- Scanning: PluginManager scans configured directories
- Manifest Detection: Looks for
plugin.jsonfiles - Loading: Dynamically imports plugin modules
- Registration: Registers workflows and tools automatically
# .env configuration
PLUGINS_DISCOVERY_PATHS=./src/workflows,./src/plugins,./plugins
PLUGINS_AUTOLOAD=true
PLUGINS_WATCH_CHANGES=false
PLUGINS_ALLOWED_LIST=my-plugin,another-plugin
PLUGINS_BLOCKED_LIST=disabled-plugin// Plugin discovery in action
const configManager = new ConfigManager()
const pluginConfig = configManager.loadPluginsConfig()
const pluginManager = new PluginManager(registry, pluginConfig, logger)
// Discover and load plugins
if (pluginConfig.autoload) {
const discoveredPlugins = await pluginManager.discoverPlugins()
logger.info('Plugins discovered', { count: discoveredPlugins.length })
}| Variable | Purpose | Default | Example |
|---|---|---|---|
PLUGINS_DISCOVERY_PATHS |
Directories to scan | ./plugins |
./src/workflows,./plugins |
PLUGINS_AUTOLOAD |
Auto-load discovered plugins | true |
true |
PLUGINS_WATCH_CHANGES |
Watch for file changes | false |
true (dev only) |
PLUGINS_ALLOWED_LIST |
Whitelist of allowed plugins | (all) | plugin1,plugin2 |
PLUGINS_BLOCKED_LIST |
Blacklist of blocked plugins | (none) | old-plugin,test-plugin |
- Workflow Directories:
./src/workflows- Looks forplugin.tsfiles - Plugin Directories:
./src/plugins- Looks forplugin.json+ main file - External Plugins:
./plugins- Third-party plugin packages - Node Modules:
./node_modules/@company/*- NPM-installed plugins
- ✅ Operation is single-step
- ✅ No complex state management needed
- ✅ Direct API mapping
- ✅ Fast response required
- ✅ Minimal business logic
- ✅ Multi-step processes
- ✅ Business logic orchestration
- ✅ Error recovery needed
- ✅ Audit trail required
- ✅ Complex validation
- ✅ State management needed
- ✅ Bundling related functionality
- ✅ Distributing to multiple projects
- ✅ Organizing large codebases
- ✅ Version management needed
- ✅ Dependency management required
- Start Simple: Begin with tools, evolve to workflows as complexity grows
- Clear Boundaries: Separate infrastructure from business logic
- Type Safety: Use TypeScript and Zod for all interfaces
- Error Handling: Implement comprehensive error handling
- Testing: Write tests for all components
- Documentation: Document all public interfaces
- Versioning: Use semantic versioning for plugins
- Dependencies: Minimize external dependencies
- Tools: Optimized for speed, minimal overhead
- Workflows: Accept longer execution times for reliability
- Plugins: Consider lazy loading for large plugin collections
- Caching: Implement caching for frequently accessed data
- Batching: Use batching for bulk operations
// Simple tools for quick operations
class EcommerceTools {
registerGetProduct(registry: ToolRegistry) {
registry.registerTool('get_product', {
title: 'Get Product',
description: 'Get product by ID',
inputSchema: { productId: z.string() }
}, async (args) => {
const product = await this.api.getProduct(args.productId)
return { content: [{ type: 'text', text: JSON.stringify(product) }] }
})
}
}
// Workflow for complex order processing
class ProcessOrderWorkflow extends WorkflowBase {
async execute(params: OrderParams): Promise<WorkflowResult> {
// 1. Validate customer
// 2. Check inventory
// 3. Process payment
// 4. Create order
// 5. Send confirmation
// 6. Update inventory
}
}// Plugin structure
crm-plugin/
├── plugin.json
├── main.ts
├── workflows/
│ ├── LeadNurturingWorkflow.ts
│ └── SalesProcessWorkflow.ts
├── tools/
│ ├── ContactTools.ts
│ └── OpportunityTools.ts
└── types/
└── crm.types.ts// Development utilities
class DevTools {
registerHealthCheck(registry: ToolRegistry) {
registry.registerTool('health_check', {
title: 'Health Check',
description: 'Check system health'
}, async () => {
const health = await this.checkSystemHealth()
return { content: [{ type: 'text', text: JSON.stringify(health) }] }
})
}
}
// Complex deployment workflow
class DeploymentWorkflow extends WorkflowBase {
async execute(params: DeploymentParams): Promise<WorkflowResult> {
// 1. Run tests
// 2. Build application
// 3. Deploy to staging
// 4. Run integration tests
// 5. Deploy to production
// 6. Verify deployment
// 7. Send notifications
}
}- Choose Your Approach: Decide between tools, workflows, or plugins based on your needs
- Set Up Environment: Configure plugin discovery paths in your
.envfile - Create Components: Implement your tools, workflows, or plugins
- Test Integration: Verify everything works with the plugin discovery system
- Document Usage: Create clear documentation for your implementations
For more examples, see the example/ directory in this repository, which demonstrates all three approaches in a working MCP server implementation.