Skip to content

Adapters: Discovery + Loading System #53

@prosdev

Description

@prosdev

🎯 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 AdapterContext interface and factory
  • Update MCP server to use discovery system

Adapter Interface

  • Define Adapter TypeScript interface
  • Define Tool TypeScript interface
  • Define AdapterContext TypeScript interface
  • Export interfaces from @lytics/dev-agent-mcp package
  • 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_modules packages
  • 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

Estimate: 1-1.5 days
Priority: High (core functionality)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions