28 KiB
MCP/Plugin Compatibility Layer
The Compatibility Layer enables oh-my-claudecode to discover, register, and use external plugins, MCP servers, and tools. It provides a unified interface for managing external tools while maintaining security through an integrated permission system.
Table of Contents
- Overview
- Architecture
- Plugin Discovery
- MCP Server Discovery
- Plugin Manifest Format
- Tool Registration
- Permission System
- MCP Bridge
- API Reference
- Examples
- Troubleshooting
Overview
The Compatibility Layer consists of four integrated systems working together:
- Discovery System - Automatically finds plugins and MCP servers from user directories
- Tool Registry - Central hub that registers and manages all external tools with conflict resolution
- Permission Adapter - Integrates with OMC's permission system for safe tool execution
- MCP Bridge - Connects to MCP servers and exposes their tools for use
Plugins MCP Configs OMC Tools
↓ ↓ ↓
Discovery System ────────────────────────────┐
↓
Tool Registry ← ← ←┘
↓
Permission Adapter
↓
MCP Bridge
Architecture
Discovery System (discovery.ts)
Scans for external plugins and MCP servers from:
~/.claude/plugins/- OMC/Claude Code plugins directory~/.claude/installed-plugins/- Alternative plugins location~/.claude/settings.json- Claude Code MCP server configs~/.claude/claude_desktop_config.json- Claude Desktop MCP server configs- Plugin manifests (
plugin.json) for embedded MCP servers
Discovers:
- Plugin skills and agents (from SKILL.md and agent .md files)
- MCP server configurations
- Tool definitions from plugin manifests
Tool Registry (registry.ts)
Central hub for tool management:
- Registers tools from discovered plugins and MCP servers
- Handles tool name conflicts using priority-based resolution
- Routes commands to appropriate handlers
- Provides search and filtering capabilities
- Emits events for registration and connection status
Key features:
- Tools are namespaced (e.g.,
plugin-name:tool-name) - Priority system for conflict resolution (higher priority wins)
- Short name lookup (finds
tool-nameeven with namespace) - Event listeners for monitoring registry state
Permission Adapter (permission-adapter.ts)
Integrates external tools with OMC's permission system:
- Maintains safe patterns for read-only tools
- Auto-approves known-safe operations
- Prompts user for dangerous operations (write, execute)
- Caches permission decisions
- Determines delegation targets for tool execution
Safe patterns:
- Built-in patterns for common MCP tools (filesystem read, context7 queries)
- Plugin-contributed patterns from manifests
- Custom patterns can be registered at runtime
MCP Bridge (mcp-bridge.ts)
Manages MCP server connections:
- Spawns server processes
- Sends JSON-RPC requests and handles responses
- Discovers tools and resources from servers
- Routes tool invocations to servers
- Handles connection lifecycle (connect, disconnect, reconnect)
Protocol: JSON-RPC 2.0 over process stdio with newline-delimited messages
Plugin Discovery
Directory Structure
Plugins are discovered from ~/.claude/plugins/ and ~/.claude/installed-plugins/:
~/.claude/plugins/
├── my-plugin/
│ ├── plugin.json (required)
│ ├── skills/ (optional)
│ │ ├── skill-1/
│ │ │ └── SKILL.md
│ │ └── skill-2/
│ │ └── SKILL.md
│ ├── agents/ (optional)
│ │ ├── agent-1.md
│ │ └── agent-2.md
│ └── commands/ (optional)
└── another-plugin/
└── plugin.json
Plugin Manifest Structure
The plugin.json defines the plugin's metadata and tools:
{
"name": "my-plugin",
"version": "1.0.0",
"description": "My awesome plugin",
"namespace": "my-plugin",
"skills": "./skills/",
"agents": "./agents/",
"commands": "./commands/",
"mcpServers": {
"server-name": {
"command": "node",
"args": ["server.js"],
"env": {},
"enabled": true,
"description": "My MCP server"
}
},
"permissions": [
{
"tool": "my-plugin:search",
"scope": "read",
"patterns": [".*"],
"reason": "Search is read-only"
}
],
"tools": [
{
"name": "my-tool",
"description": "Does something useful",
"handler": "tools/my-tool.js",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
}
}
}
]
}
Skill and Agent Discovery
Skills are discovered from SKILL.md files in supported skills directories. OMC's canonical project-local write target remains .omc/skills/, and it also reads Claude Code project skills from .claude/skills/ plus project-local compatibility skills from .agents/skills/. Each skill directory must contain a SKILL.md with frontmatter:
---
name: my-skill
description: Describes what this skill does
tags: tag1, tag2
---
Skill documentation here...
Agents are discovered from .md files in the agents directory with similar frontmatter structure. Supported runtime fields depend on Claude Code; OMC's bundled agent files currently rely on name, description, model, optional tool restrictions, and prompt body guidance. They do not currently ship an effort: frontmatter override, so effort inherits from the parent Claude Code session unless a custom agent explicitly adds one.
MCP Server Discovery
Claude Desktop Config
Located at ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/"],
"enabled": true
},
"web": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-web"],
"enabled": true
}
}
}
Claude Code Settings
Located at ~/.claude/settings.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["server.js"],
"env": {
"API_KEY": "secret"
}
}
}
}
Remote MCP / Remote OMC Shape
OMC can sync and preserve remote MCP entries in the unified registry. That is the supported narrow answer to "connect to a remote OMC":
{
"mcpServers": {
"remoteOmc": {
"url": "https://lab.example.com/mcp",
"timeout": 30
}
}
}
This supports remote MCP endpoints. It does not create a general multi-host OMC cluster or a transparent shared remote filesystem view.
Plugin-Embedded MCP Servers
Plugins can define MCP servers in their manifest:
{
"name": "plugin-with-server",
"mcpServers": {
"my-mcp": {
"command": "node",
"args": ["./mcp/server.js"]
}
}
}
Plugin Manifest Format
Complete Schema
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Plugin name (alphanumeric, hyphens, underscores) |
version |
string | Yes | Semantic version (e.g., "1.0.0") |
description |
string | No | Human-readable description |
namespace |
string | No | Prefix for tool names (defaults to plugin name) |
skills |
string|string[] | No | Path(s) to skills directory |
agents |
string|string[] | No | Path(s) to agents directory |
commands |
string|string[] | No | Path(s) to commands directory |
mcpServers |
object | No | MCP server configurations (name → McpServerEntry) |
permissions |
PluginPermission[] | No | Permissions needed for plugin tools |
tools |
PluginToolDefinition[] | No | Tool definitions |
McpServerEntry
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | Command to run server (e.g., "node", "npx") |
args |
string[] | No | Command arguments |
env |
object | No | Environment variables to pass to server |
enabled |
boolean | No | Whether server connects on init (default: true) |
description |
string | No | Human-readable description |
PluginPermission
| Field | Type | Description |
|---|---|---|
tool |
string | Tool name requiring permission |
scope |
"read"|"write"|"execute"|"all" | Permission scope |
patterns |
string[] | Regex patterns for allowed paths/commands |
reason |
string | Why this permission is needed |
PluginToolDefinition
| Field | Type | Description |
|---|---|---|
name |
string | Tool name (becomes namespace:name) |
description |
string | Human-readable description |
handler |
string | Path to handler function or command |
inputSchema |
object | JSON Schema for tool input |
Tool Registration
Registration Process
Tools are registered in this order:
- Plugin discovery - Plugins found in configured paths
- Tool extraction - Skills, agents, and tool definitions extracted from plugins
- MCP server discovery - MCP servers found from config files
- Tool conversion - MCP tools converted to ExternalTool format
- Conflict resolution - Tools with same name resolved by priority
Tool Naming
Tools use a namespaced format:
{namespace}:{tool-name}
Examples:
- my-plugin:search
- filesystem:read_file
- context7:query-docs
Short names also work:
getRegistry().getTool('search') // Finds 'my-plugin:search'
getRegistry().getTool('my-plugin:search') // Exact match
Conflict Resolution
When two plugins provide a tool with the same name:
- Priority value - Tool with higher priority wins (default: 50)
- Namespace - Use full namespaced name to disambiguate
- Manual - Check conflicts and re-register with different priority
// Check for conflicts
const conflicts = registry.getConflicts();
// Get winner for conflict
const winner = conflicts[0].winner;
console.log(`${winner.source} won with priority ${winner.priority}`);
Permission System
Safe Patterns
Read-only tools are auto-approved without user prompting:
// Check if tool is safe
const result = checkPermission('mcp__filesystem__read_file');
// { allowed: true, reason: "Filesystem read (read-only)" }
Built-in safe patterns cover:
- Context7 - Documentation queries (read-only)
- Filesystem - Read operations only
- Exa - Web search (read-only, external)
Permission Check Flow
Tool invocation
↓
Check safe patterns → Allowed (no prompt needed)
↓ (not in safe patterns)
Check dangerous patterns → Ask user
↓ (not dangerous)
Check tool capabilities → Safe caps (auto-approve) or Dangerous (ask user)
↓
Execute or Deny
Auto-Approval Examples
// Read-only tools are safe
checkPermission('my-plugin:search')
// { allowed: true, reason: "Tool has safe capabilities: search" }
// Write/execute requires user confirmation
checkPermission('filesystem:write_file', { path: '/etc/passwd' })
// { allowed: false, askUser: true, reason: "Tool requires explicit permission" }
Caching Permissions
Permission decisions are cached. Users can grant or deny persistently:
// User grants permission
grantPermission('custom:dangerous-tool', { mode: 'aggressive' });
// Later calls use cached decision
checkPermission('custom:dangerous-tool', { mode: 'aggressive' });
// { allowed: true, reason: "User granted permission" }
// Clear cache when needed
clearPermissionCache();
Registering Safe Patterns
Plugins can register safe patterns in manifest:
{
"name": "my-plugin",
"permissions": [
{
"tool": "my-plugin:query-docs",
"scope": "read",
"patterns": [".*"],
"reason": "Documentation lookup is read-only"
}
]
}
These are automatically integrated when the plugin is initialized.
MCP Bridge
Connecting to Servers
import { getMcpBridge } from './compatibility';
const bridge = getMcpBridge();
// Connect to a single server
const tools = await bridge.connect('filesystem');
console.log(`Connected. Available tools: ${tools.map(t => t.name).join(', ')}`);
// Auto-connect all enabled servers
const results = await bridge.autoConnect();
for (const [serverName, tools] of results) {
console.log(`${serverName}: ${tools.length} tools`);
}
Invoking Tools
// Invoke a tool on an MCP server
const result = await bridge.invokeTool('filesystem', 'read_file', {
path: '/home/user/.bashrc'
});
if (result.success) {
console.log('File contents:', result.data);
console.log('Time:', result.executionTime, 'ms');
} else {
console.error('Error:', result.error);
}
Reading Resources
Some MCP servers provide resources (documents, APIs, etc.):
// Read a resource
const result = await bridge.readResource('web', 'https://example.com');
if (result.success) {
console.log(result.data);
}
Connection Management
// Check connection status
if (bridge.isConnected('filesystem')) {
console.log('Connected to filesystem server');
}
// Get all server tools and resources
const tools = bridge.getServerTools('filesystem');
const resources = bridge.getServerResources('web');
// Disconnect from server
bridge.disconnect('filesystem');
// Disconnect from all servers
bridge.disconnectAll();
Events
Monitor bridge activity:
const bridge = getMcpBridge();
bridge.on('server-connected', ({ server, toolCount }) => {
console.log(`Connected to ${server} with ${toolCount} tools`);
});
bridge.on('server-disconnected', ({ server, code }) => {
console.log(`Disconnected from ${server}`);
});
bridge.on('server-error', ({ server, error }) => {
console.error(`Error from ${server}:`, error);
});
API Reference
Initialization
import {
initializeCompatibility,
getRegistry,
getMcpBridge
} from './compatibility';
// Initialize everything
const result = await initializeCompatibility({
pluginPaths: ['~/.claude/plugins'],
mcpConfigPath: '~/.claude/claude_desktop_config.json',
autoConnect: true // Auto-connect to MCP servers
});
console.log(`Plugins: ${result.pluginCount}`);
console.log(`MCP servers: ${result.mcpServerCount}`);
console.log(`Tools: ${result.toolCount}`);
console.log(`Connected: ${result.connectedServers.join(', ')}`);
Discovery Functions
import {
discoverPlugins,
discoverMcpServers,
discoverAll,
isPluginInstalled,
getPluginInfo,
getPluginPaths,
getMcpConfigPath
} from './compatibility';
// Discover plugins from custom paths
const plugins = discoverPlugins({
pluginPaths: ['/custom/plugins/path']
});
// Discover MCP servers
const servers = discoverMcpServers({
mcpConfigPath: '~/.claude/claude_desktop_config.json',
settingsPath: '~/.claude/settings.json'
});
// Discover everything at once
const result = discoverAll({
force: true // Force re-discovery even if cached
});
// Check plugin installation
if (isPluginInstalled('my-plugin')) {
const info = getPluginInfo('my-plugin');
console.log(`${info.name} v${info.version}`);
}
// Get configured paths
const pluginPaths = getPluginPaths();
const mcpPath = getMcpConfigPath();
Registry Functions
import {
getRegistry,
initializeRegistry,
routeCommand,
getExternalTool,
listExternalTools,
hasExternalPlugins,
hasMcpServers
} from './compatibility';
const registry = getRegistry();
// Register discovery and tools
await initializeRegistry({ force: true });
// Access tools
const allTools = listExternalTools();
const tool = getExternalTool('my-plugin:search');
// Route command
const route = routeCommand('search');
if (route) {
console.log(`Handler: ${route.handler}`);
console.log(`Requires permission: ${route.requiresPermission}`);
}
// Check what's available
if (hasExternalPlugins()) {
console.log('External plugins available');
}
if (hasMcpServers()) {
console.log('MCP servers available');
}
// Get all plugins and servers
const plugins = registry.getAllPlugins();
const servers = registry.getAllMcpServers();
// Search tools
const results = registry.searchTools('filesystem');
// Listen to events
registry.addEventListener(event => {
if (event.type === 'tool-registered') {
console.log(`Registered: ${event.data.tool}`);
}
});
Permission Functions
import {
checkPermission,
grantPermission,
denyPermission,
clearPermissionCache,
addSafePattern,
getSafePatterns,
shouldDelegate,
getDelegationTarget,
integrateWithPermissionSystem,
processExternalToolPermission
} from './compatibility';
// Check if tool is allowed
const check = checkPermission('my-tool:dangerous-op');
if (check.allowed) {
console.log('Allowed:', check.reason);
} else if (check.askUser) {
console.log('Ask user:', check.reason);
}
// Cache user decisions
grantPermission('custom:tool', { mode: 'aggressive' });
denyPermission('risky:tool');
clearPermissionCache();
// Manage safe patterns
const patterns = getSafePatterns();
addSafePattern({
tool: 'my-safe-tool',
pattern: /^\/safe\/path/,
description: 'Only allows /safe/path',
source: 'myapp'
});
// Check if tool should be delegated
if (shouldDelegate('external:tool')) {
const target = getDelegationTarget('external:tool');
console.log(`Delegate to: ${target.type}/${target.target}`);
}
// Integrate with permission system at startup
integrateWithPermissionSystem();
MCP Bridge Functions
import {
getMcpBridge,
resetMcpBridge,
invokeMcpTool,
readMcpResource
} from './compatibility';
const bridge = getMcpBridge();
// Connect to server
const tools = await bridge.connect('filesystem');
// Invoke tool
const result = await invokeMcpTool('filesystem', 'read_file', {
path: '/etc/hosts'
});
// Read resource
const resourceResult = await readMcpResource('web', 'https://api.example.com');
// Check connections
const status = bridge.getConnectionStatus();
// Clean up
bridge.disconnectAll();
resetMcpBridge();
Examples
Example 1: Initialize and List Tools
import { initializeCompatibility, getRegistry } from './compatibility';
async function listAvailableTools() {
// Initialize the compatibility layer
const result = await initializeCompatibility({
autoConnect: true
});
console.log(`Discovered ${result.pluginCount} plugins`);
console.log(`Connected to ${result.connectedServers.length} MCP servers`);
// List all available tools
const registry = getRegistry();
const tools = registry.getAllTools();
console.log('\nAvailable tools:');
for (const tool of tools) {
console.log(` ${tool.name} (${tool.type})`);
console.log(` Description: ${tool.description}`);
console.log(` Capabilities: ${tool.capabilities?.join(', ')}`);
}
}
listAvailableTools().catch(console.error);
Example 2: Search and Use a Tool
import {
initializeCompatibility,
getRegistry,
checkPermission,
getMcpBridge
} from './compatibility';
async function searchAndRead() {
await initializeCompatibility();
const registry = getRegistry();
// Search for filesystem tools
const fileTools = registry.searchTools('filesystem');
console.log(`Found ${fileTools.length} filesystem tools`);
// Find read_file tool
const readTool = fileTools.find(t => t.name.includes('read'));
if (readTool) {
// Check permission
const perm = checkPermission(readTool.name);
if (perm.allowed) {
const bridge = getMcpBridge();
const result = await bridge.invokeTool(
readTool.source,
'read_file',
{ path: '/etc/hosts' }
);
if (result.success) {
console.log('File contents:', result.data);
}
}
}
}
searchAndRead().catch(console.error);
Example 3: Handle Plugin with MCP Server
import {
discoverPlugins,
initializeRegistry,
getMcpBridge
} from './compatibility';
async function setupPluginMcp() {
// Discover plugins (includes MCP servers defined in manifests)
const plugins = discoverPlugins();
const pluginWithMcp = plugins.find(p => p.manifest.mcpServers);
if (pluginWithMcp) {
console.log(`Plugin ${pluginWithMcp.name} has embedded MCP servers:`);
for (const serverName of Object.keys(pluginWithMcp.manifest.mcpServers || {})) {
console.log(` - ${serverName}`);
}
// Initialize registry (registers MCP servers from plugins)
await initializeRegistry();
// Connect to plugin's MCP server
const bridge = getMcpBridge();
const fullServerName = `${pluginWithMcp.name}:${serverName}`;
try {
const tools = await bridge.connect(fullServerName);
console.log(`Connected to ${fullServerName} with ${tools.length} tools`);
} catch (err) {
console.error('Failed to connect:', err.message);
}
}
}
setupPluginMcp().catch(console.error);
Example 4: Conflict Resolution
import { getRegistry } from './compatibility';
function showConflicts() {
const registry = getRegistry();
const conflicts = registry.getConflicts();
if (conflicts.length === 0) {
console.log('No tool conflicts');
return;
}
console.log(`Found ${conflicts.length} conflicts:\n`);
for (const conflict of conflicts) {
console.log(`Tool: ${conflict.name}`);
console.log(` Winner: ${conflict.winner.source} (priority: ${conflict.winner.priority})`);
console.log(' Alternatives:');
for (const tool of conflict.tools) {
if (tool !== conflict.winner) {
console.log(` - ${tool.source} (priority: ${tool.priority})`);
}
}
console.log();
}
}
showConflicts();
Example 5: Custom Permission Pattern
import {
addSafePattern,
checkPermission,
getSafePatterns
} from './compatibility';
function registerCustomPatterns() {
// Register a safe pattern for a plugin tool
addSafePattern({
tool: 'analytics:track',
pattern: /^(page_view|event|error)$/,
description: 'Only allows tracking specific event types',
source: 'myapp'
});
// Now check permission with valid input
let result = checkPermission('analytics:track');
console.log('Safe:', result.allowed); // true
// View all patterns
const patterns = getSafePatterns();
const myPatterns = patterns.filter(p => p.source === 'myapp');
console.log('My patterns:', myPatterns.length);
}
registerCustomPatterns();
Troubleshooting
Plugins Not Discovered
Problem: discoverPlugins() returns empty array.
Checklist:
- Plugins are in
~/.claude/plugins/or~/.claude/installed-plugins/ - Each plugin has a
plugin.jsonin the root or.claude-plugin/subdirectory - Plugin name doesn't conflict with reserved names (e.g., 'oh-my-claudecode')
- File permissions allow reading the directory
Debug:
import { getPluginPaths } from './compatibility';
const paths = getPluginPaths();
console.log('Scanning paths:', paths);
// Check if directory exists
import { existsSync } from 'fs';
for (const path of paths) {
console.log(`${path}: ${existsSync(path) ? 'exists' : 'missing'}`);
}
MCP Server Won't Connect
Problem: bridge.connect() times out.
Checklist:
- Server command is correct (e.g.,
npx,node) - Command is executable and in PATH
- Arguments are valid
- Server implements MCP protocol (JSON-RPC 2.0)
- Check stderr output for errors
Debug:
import { getMcpBridge } from './compatibility';
const bridge = getMcpBridge();
bridge.on('server-error', ({ server, error }) => {
console.error(`Server error from ${server}:`, error);
});
bridge.on('connect-error', ({ server, error }) => {
console.error(`Failed to connect to ${server}:`, error);
});
Tools Not Showing Up
Problem: Registered tools don't appear in getRegistry().getAllTools().
Causes and solutions:
- Plugin not discovered - Check plugin discovery first
- Tools not extracted - Ensure SKILL.md files exist in skills directory
- Namespace conflict - Two plugins with same namespace
- Tool registration failed - Check registry events for errors
Debug:
import { getRegistry, discoverPlugins } from './compatibility';
const plugins = discoverPlugins();
for (const plugin of plugins) {
console.log(`${plugin.name}: ${plugin.tools.length} tools`);
for (const tool of plugin.tools) {
console.log(` - ${tool.name}`);
}
}
// Check what's actually registered
const registry = getRegistry();
const registered = registry.getAllTools();
console.log(`Registry has ${registered.length} tools`);
// Listen for registration events
registry.addEventListener(event => {
if (event.type === 'tool-registered') {
console.log('Registered:', event.data.tool);
} else if (event.type === 'tool-conflict') {
console.log('Conflict:', event.data.name, '→', event.data.winner);
}
});
Permission Always Denied
Problem: Tools requiring permission always get denied even after user approval.
Solutions:
- Clear permission cache:
clearPermissionCache() - Ensure you're using same tool name/input for cached decision
- Check if tool matches a dangerous pattern that overrides caching
Debug:
import {
checkPermission,
grantPermission,
getSafePatterns
} from './compatibility';
// Check if tool is in dangerous patterns
const patterns = getSafePatterns();
console.log('Safe patterns:', patterns.length);
// Manually grant
grantPermission('my-tool');
// Verify it's cached
const result = checkPermission('my-tool');
console.log('Allowed:', result.allowed);
console.log('Reason:', result.reason);
Manifest Parse Errors
Problem: Plugin loads but manifest parsing fails.
Checklist:
plugin.jsonis valid JSON (usenpm install -g jsonlintto validate)- Required fields present:
name,version - No syntax errors in paths or configs
- File encoding is UTF-8
Debug:
import { getPluginInfo } from './compatibility';
const plugin = getPluginInfo('my-plugin');
if (plugin && !plugin.loaded) {
console.error('Failed to load:', plugin.error);
console.log('Manifest:', plugin.manifest);
}
MCP Tool Invocation Fails
Problem: Tool invocation returns error.
Debug:
import { getMcpBridge } from './compatibility';
const bridge = getMcpBridge();
// Check connection
console.log('Connected:', bridge.isConnected('myserver'));
// Get available tools
const tools = bridge.getServerTools('myserver');
console.log('Available tools:', tools.map(t => t.name));
// Try invocation with error details
const result = await bridge.invokeTool('myserver', 'tool-name', {});
if (!result.success) {
console.error('Error:', result.error);
console.error('Time:', result.executionTime, 'ms');
}
Best Practices
- Initialize early - Call
initializeCompatibility()on startup - Cache registry - Reuse
getRegistry()instance, don't repeatedly initialize - Handle permissions gracefully - Always check
checkPermission()before invoking dangerous tools - Monitor events - Use event listeners to track plugin/server status changes
- Version check - Include version constraints in plugin manifests for compatibility
- Test plugins locally - Before publishing, test with local discovery paths
- Use namespaces - Set
namespacein manifest to avoid conflicts - Document permissions - Clearly explain why plugins need specific scopes
- Handle errors - MCP connections can fail; implement retry logic
- Clean up - Call
disconnectAll()andresetMcpBridge()on shutdown