-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
🎯 Overview
Implement adapter discovery and loading system that supports built-in, npm, and local adapters. This enables per-project adapter configuration and extensibility.
Part of Epic: #31
Depends on: #52 (Foundation: Storage + Config)
🔍 Adapter Discovery
Discovery Algorithm
The system discovers adapters based on project configuration:
async function discoverAdapters(
adapterConfig: Record<string, AdapterConfig>,
repoPath: string
): Promise<Adapter[]> {
const adapters = [];
for (const [name, config] of Object.entries(adapterConfig)) {
if (!config.enabled) continue;
// Built-in adapter (shipped with dev-agent)
if (BUILTIN_ADAPTERS.includes(name)) {
adapters.push(loadBuiltInAdapter(name));
continue;
}
// Custom adapter
const source = config.source;
if (!source) {
throw new Error(`Adapter '${name}' requires 'source' property`);
}
if (source.startsWith('./') || source.startsWith('../')) {
// Local adapter: ./adapters/datadog
const adapterPath = path.resolve(repoPath, source);
adapters.push(await loadLocalAdapter(adapterPath, config.settings));
} else {
// NPM adapter: @company/mcp-adapter-jira
adapters.push(await loadNpmAdapter(source, config.settings));
}
}
return adapters;
}Built-in Adapters
const BUILTIN_ADAPTERS = [
'search',
'github',
'plan',
'explore',
'status'
];
function loadBuiltInAdapter(name: string): Adapter {
switch (name) {
case 'search':
return createSearchAdapter(context);
case 'github':
return createGitHubAdapter(context);
case 'plan':
return createPlanAdapter(context);
case 'explore':
return createExploreAdapter(context);
case 'status':
return createStatusAdapter(context);
default:
throw new Error(`Unknown built-in adapter: ${name}`);
}
}Local Adapter Loading
async function loadLocalAdapter(
adapterPath: string,
settings?: Record<string, unknown>
): Promise<Adapter> {
// Check if adapter exists
if (!fs.existsSync(adapterPath)) {
throw new Error(`Adapter not found: ${adapterPath}`);
}
// Load adapter entry point
const adapterModule = await import(adapterPath);
const createAdapter = adapterModule.default || adapterModule.createAdapter;
if (typeof createAdapter !== 'function') {
throw new Error(`Adapter ${adapterPath} must export default function`);
}
// Create adapter with context
const context = createAdapterContext(settings);
return createAdapter(context);
}NPM Adapter Loading
async function loadNpmAdapter(
packageName: string,
settings?: Record<string, unknown>
): Promise<Adapter> {
// Resolve package from node_modules
let adapterPath: string;
try {
adapterPath = require.resolve(`${packageName}/dist/index.js`);
} catch {
try {
adapterPath = require.resolve(`${packageName}/index.js`);
} catch {
throw new Error(
`Adapter package '${packageName}' not found. Run 'npm install ${packageName}'`
);
}
}
// Load adapter
const adapterModule = await import(adapterPath);
const createAdapter = adapterModule.default || adapterModule.createAdapter;
if (typeof createAdapter !== 'function') {
throw new Error(
`Adapter ${packageName} must export default function`
);
}
// Create adapter with context
const context = createAdapterContext(settings);
return createAdapter(context);
}🔌 Adapter Interface
Adapter Definition
interface Adapter {
name: string;
version: string;
tools: Tool[];
onActivate?: () => Promise<void>;
onDeactivate?: () => Promise<void>;
}
interface Tool {
name: string;
description: string;
inputSchema: JSONSchema;
handler: (args: Record<string, unknown>) => Promise<unknown>;
}
interface AdapterContext {
config: {
repositoryPath: string;
settings?: Record<string, unknown>;
};
logger: Logger;
indexer: RepositoryIndexer;
secrets: SecretsManager;
}Adapter Context
function createAdapterContext(
settings?: Record<string, unknown>
): AdapterContext {
return {
config: {
repositoryPath: process.env.REPOSITORY_PATH || process.cwd(),
settings,
},
logger: getLogger(),
indexer: getIndexer(), // Lazy-loaded
secrets: new SecretsManager(),
};
}📦 Example Adapters
Simple Local Adapter
// .dev-agent/adapters/datadog/index.ts
import type { AdapterContext, Tool } from '@lytics/dev-agent-mcp';
export default function createDatadogAdapter(context: AdapterContext) {
const { config, logger, secrets } = context;
return {
name: 'datadog',
version: '1.0.0',
tools: [
{
name: 'datadog_query',
description: 'Query Datadog logs',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
from: { type: 'number' },
to: { type: 'number' }
},
required: ['query']
},
async handler(args) {
const apiKey = await secrets.get('DATADOG_API_KEY');
const site = config.settings?.site || 'us5.datadoghq.com';
const response = await fetch(
`https://api.${site}/api/v1/logs-queries/list`,
{
headers: {
'DD-API-KEY': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: args.query,
time: { from: args.from, to: args.to }
})
}
);
return response.json();
}
}
],
async onActivate() {
logger.info('Datadog adapter activated');
}
};
}NPM Package Adapter
// @company/mcp-adapter-jira/src/index.ts
import type { AdapterContext } from '@lytics/dev-agent-mcp';
export default function createJiraAdapter(context: AdapterContext) {
return {
name: 'jira',
version: '1.0.0',
tools: [
{
name: 'jira_search',
description: 'Search Jira issues using JQL',
inputSchema: {
type: 'object',
properties: {
jql: { type: 'string' }
},
required: ['jql']
},
async handler(args) {
// Implementation
}
}
]
};
}🏗️ MCP Server Integration
Updated Server Initialization
// packages/mcp-server/bin/dev-agent-mcp.ts
async function main() {
// 1. Get repository path from env (set by Cursor)
const repositoryPath = process.env.REPOSITORY_PATH || process.cwd();
// 2. Load project config
const config = await loadConfig(
path.join(repositoryPath, '.dev-agent/config.json')
);
// 3. Discover adapters based on project config
const adapters = await discoverAdapters(
config.mcp?.adapters || {},
repositoryPath
);
// 4. Start MCP server with project-specific adapters
const server = new MCPServer({
config: { repositoryPath },
adapters: adapters, // Different per project!
});
await server.start();
}📋 Implementation Tasks
Core Discovery System
- Implement
discoverAdapters()function - Implement
loadBuiltInAdapter()function - Implement
loadLocalAdapter()function - Implement
loadNpmAdapter()function - Create
AdapterContextinterface and factory - Update MCP server to use discovery system
Adapter Interface
- Define
AdapterTypeScript interface - Define
ToolTypeScript interface - Define
AdapterContextTypeScript interface - Export interfaces from
@lytics/dev-agent-mcppackage - Create adapter base class/utilities (optional)
Error Handling
- Handle missing adapter files gracefully
- Handle invalid adapter exports (not a function)
- Handle missing npm packages with helpful errors
- Handle adapter initialization errors
- Log adapter loading failures without crashing server
Testing
- Unit tests for built-in adapter loading
- Unit tests for local adapter loading
- Unit tests for npm adapter loading
- Integration test for full discovery flow
- Test error handling for invalid adapters
✅ Acceptance Criteria
- Built-in adapters load correctly from config
- Local adapters load from relative paths (
./adapters/name) - NPM adapters load from
node_modulespackages - Disabled adapters are skipped
- Invalid adapters fail gracefully with helpful errors
- Adapter context provides config, logger, indexer, secrets
- MCP server initializes with discovered adapters
- Adapter tools are registered correctly in MCP server
🧪 Testing Strategy
Unit Tests
- Test each discovery function independently
- Mock file system and module resolution
- Test error cases (missing files, invalid exports)
Integration Tests
- Test full discovery flow with real config
- Test adapter tool registration
- Test adapter lifecycle hooks
🔗 Dependencies
- Depends on: Foundation: Storage + Config System #52 (Foundation: Storage + Config) - needs config system
- Blocks: UX: CLI Management Commands #54 (UX: CLI Management) - CLI needs discovery system
Estimate: 1-1.5 days
Priority: High (core functionality)
Metadata
Metadata
Assignees
Labels
No labels