# Import Detection Pattern Migration Guide

**Phase 3.3 M2**: Migrate from try/except import detection to `importlib.util.find_spec()`

**Status**: ✅ Implementation ready, 6 servers affected

______________________________________________________________________

## Overview

This guide shows how to migrate MCP servers from try/except import detection to the more idiomatic `importlib.util.find_spec()` pattern.

**Benefits**:

- ✅ **Explicit intent** - Clear that we're checking module availability
- ✅ **No exception handling** - Avoids using exceptions for control flow
- ✅ **Pythonic** - Standard library approach for module detection
- ✅ **Safer** - Won't accidentally catch ImportErrors from within the module
- ✅ **Type-safe** - Clear boolean return value

______________________________________________________________________

## Quick Migration Example

### Before (try/except pattern)

```python
# ❌ Using exceptions for control flow
try:
    from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

    RATE_LIMITING_AVAILABLE = True
except ImportError:
    RATE_LIMITING_AVAILABLE = False

# Later usage
if RATE_LIMITING_AVAILABLE:
    rate_limiter = RateLimitingMiddleware(
        max_requests_per_second=10.0,
        burst_capacity=20,
        global_limit=True,
    )
    mcp.add_middleware(rate_limiter)
```

### After (importlib pattern)

```python
# ✅ Explicit module availability check
import importlib.util

RATE_LIMITING_AVAILABLE = (
    importlib.util.find_spec("fastmcp.server.middleware.rate_limiting") is not None
)

# Later usage (conditional import)
if RATE_LIMITING_AVAILABLE:
    from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

    rate_limiter = RateLimitingMiddleware(
        max_requests_per_second=10.0,
        burst_capacity=20,
        global_limit=True,
    )
    mcp.add_middleware(rate_limiter)
```

______________________________________________________________________

## Why This Pattern is Better

### 1. **Explicit Intent**

```python
# ❌ OLD: Unclear why we're catching ImportError
try:
    import something

    AVAILABLE = True
except ImportError:
    AVAILABLE = False

# ✅ NEW: Clearly checking module existence
AVAILABLE = importlib.util.find_spec("something") is not None
```

### 2. **Avoids Exception-Based Control Flow**

Python's official style guide (PEP 8) discourages using exceptions for normal control flow. Import availability is a normal case, not exceptional.

```python
# ❌ OLD: Exception-based control flow (anti-pattern)
try:
    import module

    do_something()
except ImportError:
    pass

# ✅ NEW: Explicit conditional logic
if importlib.util.find_spec("module") is not None:
    import module

    do_something()
```

### 3. **Safer Error Handling**

Try/except can accidentally catch ImportErrors from *within* the imported module:

```python
# ❌ OLD: Catches ALL ImportErrors (including from inside the module)
try:
    from my_module import something  # If this imports numpy and numpy fails...

    AVAILABLE = True
except ImportError:
    AVAILABLE = False  # ...we'll silently treat my_module as unavailable!

# ✅ NEW: Only checks if module exists
AVAILABLE = importlib.util.find_spec("my_module") is not None
if AVAILABLE:
    from my_module import something  # ImportError here will raise properly
```

### 4. **Type Safety**

```python
# ❌ OLD: Boolean set in two places, potential for inconsistency
try:
    import module

    AVAILABLE = True  # Could forget this
except ImportError:
    AVAILABLE = False

# ✅ NEW: Single source of truth, guaranteed boolean
AVAILABLE = importlib.util.find_spec("module") is not None
```

______________________________________________________________________

## Affected Servers

Based on Phase 3 codebase analysis:

### Servers Using This Pattern (6 servers)

1. **session-mgmt-mcp** - 10+ instances

   - `MCP_AVAILABLE`
   - `SERVERPANELS_AVAILABLE`
   - `SECURITY_AVAILABLE`
   - `RATE_LIMITING_AVAILABLE`
   - `SESSION_MANAGEMENT_AVAILABLE`
   - `UTILITY_FUNCTIONS_AVAILABLE`
   - `MULTI_PROJECT_AVAILABLE`
   - `ADVANCED_SEARCH_AVAILABLE`
   - `CONFIG_AVAILABLE`
   - `APP_MONITOR_AVAILABLE`
   - `LLM_PROVIDERS_AVAILABLE`
   - `SERVERLESS_MODE_AVAILABLE`

1. **acb** - 2 instances

   - `RATE_LIMITING_AVAILABLE`
   - `SERVERPANELS_AVAILABLE`

1. **opera-cloud-mcp** - 2 instances

   - `RATE_LIMITING_AVAILABLE`
   - `SERVERPANELS_AVAILABLE`

1. **unifi-mcp** - 2 instances

   - `RATE_LIMITING_AVAILABLE`
   - `SERVERPANELS_AVAILABLE`

1. **raindropio-mcp** - 2 instances

   - `RATE_LIMITING_AVAILABLE`
   - `SERVERPANELS_AVAILABLE`

1. **excalidraw-mcp** - 1 instance

   - `SERVERPANELS_AVAILABLE`

### Servers Not Affected (3 servers)

- **mailgun-mcp**: No pattern usage
- **fastblocks**: No pattern usage
- **crackerjack**: No pattern usage

______________________________________________________________________

## Step-by-Step Migration

### Step 1: Add importlib Import

At the top of your server.py file:

```python
import importlib.util
```

### Step 2: Replace Try/Except Blocks

Find each try/except import block:

```python
# OLD (find this pattern):
try:
    from module.path import Something

    SOMETHING_AVAILABLE = True
except ImportError:
    SOMETHING_AVAILABLE = False
```

Replace with:

```python
# NEW (replace with this):
SOMETHING_AVAILABLE = importlib.util.find_spec("module.path") is not None
```

### Step 3: Move Import Inside Conditional

The actual import should move to where it's used:

```python
# NEW: Import inside conditional usage
if SOMETHING_AVAILABLE:
    from module.path import Something

    # Use Something here
    obj = Something(...)
```

### Step 4: Test Changes

```python
# Verify module detection works
assert SOMETHING_AVAILABLE in (True, False)  # Should be boolean

# Test with module present
if SOMETHING_AVAILABLE:
    from module.path import Something

    print(f"✅ Module available: {Something}")
else:
    print("⚠️ Module not available (expected in some environments)")
```

______________________________________________________________________

## Migration Patterns by Use Case

### Pattern 1: Rate Limiting Middleware

**Before**:

```python
try:
    from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

    RATE_LIMITING_AVAILABLE = True
except ImportError:
    RATE_LIMITING_AVAILABLE = False

# Usage
if RATE_LIMITING_AVAILABLE:
    rate_limiter = RateLimitingMiddleware(...)
    mcp.add_middleware(rate_limiter)
```

**After**:

```python
import importlib.util

RATE_LIMITING_AVAILABLE = (
    importlib.util.find_spec("fastmcp.server.middleware.rate_limiting") is not None
)

# Usage (import moved here)
if RATE_LIMITING_AVAILABLE:
    from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

    rate_limiter = RateLimitingMiddleware(...)
    mcp.add_middleware(rate_limiter)
```

### Pattern 2: ServerPanels UI

**Before**:

```python
try:
    from mcp_common.ui import ServerPanels

    SERVERPANELS_AVAILABLE = True
except ImportError:
    SERVERPANELS_AVAILABLE = False

# Usage
if SERVERPANELS_AVAILABLE:
    panels = ServerPanels(...)
    print(panels.render())
```

**After**:

```python
import importlib.util

SERVERPANELS_AVAILABLE = importlib.util.find_spec("mcp_common.ui") is not None

# Usage (import moved here)
if SERVERPANELS_AVAILABLE:
    from mcp_common.ui import ServerPanels

    panels = ServerPanels(...)
    print(panels.render())
```

### Pattern 3: Security Utilities

**Before**:

```python
try:
    from mcp_common.security import APIKeyValidator

    SECURITY_AVAILABLE = True
except ImportError:
    SECURITY_AVAILABLE = False

# Usage
if SECURITY_AVAILABLE:
    validator = APIKeyValidator(...)
```

**After**:

```python
import importlib.util

SECURITY_AVAILABLE = importlib.util.find_spec("mcp_common.security") is not None

# Usage (import moved here)
if SECURITY_AVAILABLE:
    from mcp_common.security import APIKeyValidator

    validator = APIKeyValidator(...)
```

______________________________________________________________________

## Testing Your Migration

### Basic Verification

```python
def test_module_detection():
    """Verify module detection works correctly."""
    import importlib.util

    # Should return boolean
    result = importlib.util.find_spec("fastmcp") is not None
    assert isinstance(result, bool)

    # Known module should be True
    assert importlib.util.find_spec("sys") is not None

    # Non-existent module should be False
    assert importlib.util.find_spec("nonexistent_module_12345") is None
```

### Integration Testing

```python
def test_conditional_import():
    """Test conditional import after detection."""
    import importlib.util

    MODULE_AVAILABLE = (
        importlib.util.find_spec("fastmcp.server.middleware.rate_limiting") is not None
    )

    if MODULE_AVAILABLE:
        # Should import without error
        from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

        assert RateLimitingMiddleware is not None
    else:
        # Should not raise ImportError
        print("Module not available (expected in some test environments)")
```

______________________________________________________________________

## Common Patterns Reference

### Single Module Check

```python
# Check if single module exists
MODULE_AVAILABLE = importlib.util.find_spec("module_name") is not None
```

### Submodule Check

```python
# Check if submodule exists
SUBMODULE_AVAILABLE = importlib.util.find_spec("package.submodule") is not None
```

### Multiple Checks

```python
# Check multiple modules
RATE_LIMITING_AVAILABLE = (
    importlib.util.find_spec("fastmcp.server.middleware.rate_limiting") is not None
)
SERVERPANELS_AVAILABLE = importlib.util.find_spec("mcp_common.ui") is not None
SECURITY_AVAILABLE = importlib.util.find_spec("mcp_common.security") is not None
```

### With Conditional Import

```python
# Check availability
FEATURE_AVAILABLE = importlib.util.find_spec("feature_module") is not None

# Conditional import and usage
if FEATURE_AVAILABLE:
    from feature_module import FeatureClass

    feature = FeatureClass()
    feature.do_something()
else:
    logger.info("Feature module not available, skipping feature")
```

______________________________________________________________________

## Migration Checklist

For each server:

- [ ] Add `import importlib.util` at top of file
- [ ] Identify all try/except import blocks
- [ ] Replace each block with `importlib.util.find_spec()` pattern
- [ ] Move imports inside conditional usage blocks
- [ ] Test module detection with and without modules present
- [ ] Verify imports work correctly when modules available
- [ ] Run existing tests to ensure no regressions
- [ ] Update code comments to reflect new pattern

______________________________________________________________________

## Troubleshooting

### Issue: `find_spec()` returns None for installed module

**Problem**:

```python
result = importlib.util.find_spec("my_module")
assert result is not None  # Fails even though module is installed
```

**Solution**: Check module name is correct:

```python
# ❌ Wrong: Using import name
importlib.util.find_spec("from_module")  # Wrong!

# ✅ Correct: Using actual module name
importlib.util.find_spec("actual_module_name")
```

### Issue: Import still fails with module available

**Problem**:

```python
AVAILABLE = importlib.util.find_spec("module") is not None  # True
if AVAILABLE:
    from module import Class  # Still raises ImportError!
```

**Solution**: Module exists but has broken dependencies. This is actually the correct behavior - `find_spec()` only checks if module *exists*, not if it can be imported. You may need a try/except for the actual import:

```python
AVAILABLE = importlib.util.find_spec("module") is not None

if AVAILABLE:
    try:
        from module import Class
    except ImportError as e:
        logger.error(f"Module exists but cannot be imported: {e}")
        AVAILABLE = False
```

______________________________________________________________________

## Performance Considerations

### find_spec() is Fast

`importlib.util.find_spec()` is very fast - it only checks if a module exists, it doesn't import it:

```python
import timeit

# find_spec is much faster than try/except import
timeit.timeit(
    'importlib.util.find_spec("sys") is not None', setup="import importlib.util", number=100000
)  # ~0.02 seconds

timeit.timeit("try: import sys\nexcept: pass", number=100000)  # ~0.15 seconds (7.5x slower!)
```

### Caching

`find_spec()` results are cached by Python's import system, so repeated checks are essentially free.

______________________________________________________________________

## See Also

- **[Phase 3 Consolidated Review](./PHASE3_CONSOLIDATED_REVIEW.md)**: M2 issue details
- **[Python importlib Documentation](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)**: Official Python docs
- **[PEP 8 Style Guide](https://peps.python.org/pep-0008/)**: Python coding standards

______________________________________________________________________

**Created**: 2025-01-27 (Phase 3.3 M2)
**Status**: ✅ Ready for implementation
**Affected Servers**: 6/9 servers (session-mgmt-mcp, acb, opera-cloud-mcp, unifi-mcp, raindropio-mcp, excalidraw-mcp)
