Pattern-035: MCP Integration Router with Adapter Methods

Status

Emerging - Proven in GitHub integration, ready for broader adoption

Context

During Phase 2 of MCP+Spatial migration (ADR-013), integration routers need to provide backward-compatible interfaces while delegating to new MCP spatial adapters. This creates a challenge:

When This Pattern Applies

Pattern Description

Add thin adapter methods to integration routers that:

  1. Provide stable, consumer-facing interface
  2. Delegate to MCP+Spatial adapters internally
  3. Support lazy initialization for async operations
  4. Translate parameter names/formats between interfaces
  5. Maintain backward compatibility during migration

Core Concept

Interface Stability via Delegation - Router provides unchanging public interface while implementation evolves to use MCP protocol and spatial intelligence.

Lazy Initialization - Router initializes async resources (like API tokens) on first use rather than during construction, using double-check locking pattern.

Implementation

Structure

GitHubIntegrationRouter (stable interface)
    ├── __init__() - sync initialization
    ├── initialize() - async initialization (idempotent)
    ├── get_recent_issues() - adapter method → list_github_issues_direct()
    ├── get_issue() - adapter method → get_github_issue_direct()
    ├── get_open_issues() - adapter method → get_recent_issues()
    └── _mcp_adapter: GitHubMCPSpatialAdapter
            ├── list_github_issues_direct() - MCP implementation
            ├── get_github_issue_direct() - MCP implementation
            └── configure_github_api() - async token setup

Code Example

# File: services/integrations/github/github_integration_router.py

class GitHubIntegrationRouter:
    """
    Integration router for GitHub operations.
    Provides stable interface while delegating to MCP+Spatial implementation.

    ARCHITECTURAL PATTERN (ADR-013 Phase 2):
    - Consumers call router methods (get_recent_issues, etc.)
    - Router delegates to MCP+Spatial adapter
    - Adapter provides 8-dimensional spatial context
    - Adapter handles MCP protocol and circuit breakers
    """

    def __init__(self, config_service: Optional[GitHubConfigService] = None):
        """Initialize router with sync operations only."""
        self.config_service = config_service or GitHubConfigService()
        self.mcp_adapter = GitHubMCPSpatialAdapter()

        # Lazy initialization tracking
        self._initialized = False
        self._initialization_lock = None  # Created in async context

    async def initialize(self):
        """
        Initialize the router asynchronously (idempotent).

        Uses double-check locking pattern to ensure:
        - Only one initialization occurs
        - Thread-safe in async context
        - Safe to call multiple times
        """
        # Skip if already initialized
        if self._initialized:
            return

        # Create lock if needed (first async call)
        if self._initialization_lock is None:
            import asyncio
            self._initialization_lock = asyncio.Lock()

        # Double-check pattern
        async with self._initialization_lock:
            if self._initialized:
                return

            # Configure MCP adapter with authentication
            token = self.config_service.get_authentication_token()
            if token:
                await self.mcp_adapter.configure_github_api(token)

            self._initialized = True

    async def get_recent_issues(
        self,
        repo: str = None,
        limit: int = 10,
        state: str = "open"
    ) -> List[Dict[str, Any]]:
        """
        Get recent issues (adapter method).

        ADAPTER METHOD: Delegates to MCP spatial adapter.
        Uses lazy initialization to ensure token loaded.
        """
        # Lazy initialization
        if not self._initialized:
            await self.initialize()

        # Delegate to MCP adapter (different method name)
        return await self.mcp_adapter.list_github_issues_direct(
            repository=repo,
            limit=limit,
            state=state
        )

    async def get_issue(
        self,
        issue_number: int,
        repo: str = None
    ) -> Optional[Dict[str, Any]]:
        """
        Get single issue by number (adapter method).

        ADAPTER METHOD: Delegates to MCP spatial adapter.
        Uses lazy initialization to ensure token loaded.
        """
        # Lazy initialization
        if not self._initialized:
            await self.initialize()

        # Delegate to MCP adapter (different method name + parameter format)
        return await self.mcp_adapter.get_github_issue_direct(
            issue_number=str(issue_number),  # MCP expects string
            repository=repo
        )

    async def get_open_issues(
        self,
        repo: str = None,
        limit: int = 10
    ) -> List[Dict[str, Any]]:
        """
        Get open issues (adapter method).

        ADAPTER METHOD: Delegates to another adapter method.
        """
        return await self.get_recent_issues(
            repo=repo,
            limit=limit,
            state="open"
        )

Usage Guidelines

When to Use

When NOT to Use

Best Practices

  1. Idempotent Initialization: Use double-check locking pattern for async init
  2. Clear Documentation: Mark adapter methods with ADAPTER METHOD comments
  3. Parameter Translation: Document any parameter format changes (e.g., int → string)
  4. Lazy Init Pattern: Initialize on first use, not in constructor
  5. Preserve Interface: Keep existing method signatures unchanged
  6. Test Both Paths: Verify adapter delegation AND lazy initialization work

Common Pitfalls

Examples in Codebase

Primary Usage

Test Examples

Complements

Alternatives

Dependencies

Migration Strategy

Phase 1: Legacy Interface (Before)

class GitHubIntegrationRouter:
    def get_recent_issues(self, limit=10):
        # Direct GitHub API calls
        return self._github_client.list_issues(limit=limit)

Phase 2: Adapter Methods (During Migration)

class GitHubIntegrationRouter:
    async def get_recent_issues(self, limit=10):
        # Adapter method delegates to MCP
        return await self.mcp_adapter.list_github_issues_direct(limit=limit)

Phase 3: MCP Only (After Migration)

# Router retired, consumers call MCP adapter directly
mcp_adapter = GitHubMCPSpatialAdapter()
issues = await mcp_adapter.list_github_issues_direct(limit=10)

Benefits

Performance Considerations

References

Documentation

Usage Analysis

Implementation Evidence


Pattern created: October 19, 2025 Sprint: A4 (CORE-STAND-FOUND) Status: Emerging (proven in production)