Skip to content

ADR-008: Package Structure - Library with Optional MCP

Status: Accepted Date: 2024-12-01 Deciders: LLM Council (Unanimous) Technical Story: Restructure package to support both library and MCP server usage patterns

Context and Problem Statement

The current package llm-council-mcp is focused on MCP (Model Context Protocol) server usage. However, users want to:

  1. Use as a Python library: from llm_council import run_full_council
  2. Use as an MCP server: llm-council CLI command

The current structure has issues: - Package name (llm-council-mcp) implies MCP-only usage - Core library functions aren't properly exported in __init__.py - All users get MCP dependencies even if they only want the library

Decision Drivers

  • User Experience: Clean imports matching package name
  • Dependency Hygiene: Don't force MCP deps on library-only users
  • Maintainability: Single repo, single version, single release pipeline
  • Python Best Practices: Follow established patterns (extras)
  • Future Flexibility: MCP is niche; don't tie identity to one protocol

Considered Options

Option A: Single Package llm-council-mcp

Keep current name, export both library and MCP functionality.

Pros: - No migration needed - Simple

Cons: - Name mismatch: pip install llm-council-mcp but from llm_council import ... - Forces MCP dependencies on everyone - Branding suggests MCP-only

Option B: Two Packages

Separate llm-council (library) and llm-council-mcp (server).

Pros: - Clean separation - Minimal dependencies for library users

Cons: - Two release cycles, version sync headaches - User confusion ("which do I install?") - Overkill for single integration

Option C: Single Package with Optional Extras

Rename to llm-council with [mcp] extra for server functionality.

Pros: - Clean naming: pip install llm-councilfrom llm_council import ... - Library users get minimal dependencies - MCP users opt-in: pip install "llm-council[mcp]" - One repo, one version - Standard Python pattern (httpx, fastapi, pandas use this)

Cons: - Requires migration from old package name - CLI always installed (must handle missing deps gracefully)

Decision Outcome

Chosen: Option C - Single package llm-council with optional [mcp] extra.

Rationale (Council Consensus)

  1. Identity: llm-council is the product; MCP is just a protocol it supports
  2. Standards: Using extras is the Python convention for optional features
  3. Maintenance: One package to maintain vs two
  4. Technical Constraint: Python extras cannot conditionally add CLI entry points, but graceful degradation handles this elegantly

Implementation

Package Structure

src/
└── llm_council/
    ├── __init__.py        # Exports: run_full_council, Council, etc.
    ├── council.py         # Core orchestration logic
    ├── openrouter.py      # LLM API client
    ├── config.py          # Configuration
    ├── cache.py           # Response caching
    ├── telemetry.py       # Telemetry protocol
    ├── cli.py             # Entry point (handles missing deps)
    └── mcp_server.py      # MCP server (optional import)

pyproject.toml

[project]
name = "llm-council"
version = "1.0.0"
description = "Multi-LLM council system with peer review and synthesis"
requires-python = ">=3.10"

dependencies = [
    "httpx>=0.25.0",
    "pydantic>=2.0.0",
    # Core dependencies only
]

[project.optional-dependencies]
mcp = [
    "mcp>=1.0.0",
    # MCP server dependencies
]

[project.scripts]
llm-council = "llm_council.cli:main"

CLI with Graceful Degradation

# llm_council/cli.py
import sys

def main():
    try:
        from llm_council.mcp_server import mcp
    except ImportError:
        print("Error: MCP dependencies not installed.", file=sys.stderr)
        print("\nTo use the MCP server, install with:", file=sys.stderr)
        print("    pip install 'llm-council[mcp]'", file=sys.stderr)
        sys.exit(1)

    mcp.run()

Public API Exports

# llm_council/__init__.py
from llm_council.council import (
    run_full_council,
    stage1_collect_responses,
    stage2_collect_rankings,
    stage3_synthesize_final,
)
from llm_council.config import CouncilConfig

__all__ = [
    "run_full_council",
    "stage1_collect_responses",
    "stage2_collect_rankings",
    "stage3_synthesize_final",
    "CouncilConfig",
]

Migration Strategy

Phase 1: Publish New Package

  1. Rename package to llm-council
  2. Restructure with optional MCP extras
  3. Export core functions in __init__.py
  4. Publish to PyPI

Phase 2: Deprecate Old Package

  1. Final release of llm-council-mcp v0.x.x
  2. Make it depend on llm-council[mcp]
  3. Add deprecation warning on import
  4. Update README with migration instructions

Phase 3: Sunset

  1. After 6 months, mark llm-council-mcp as deprecated on PyPI
  2. Remove from active maintenance

Usage Examples

Library Usage

pip install llm-council
from llm_council import run_full_council

stage1, stage2, stage3, metadata = await run_full_council(
    "What's the best approach for error handling?"
)
print(stage3["response"])

MCP Server Usage

pip install "llm-council[mcp]"
llm-council

Claude Code Integration

claude mcp add llm-council -- llm-council

Consequences

Positive

  • Clean, memorable package name
  • Library users get minimal install
  • Single package to maintain
  • Follows Python best practices
  • Future-proof for additional protocols/interfaces

Negative

  • Migration required for existing users
  • CLI installed even for library-only users (gracefully handles missing deps)
  • Old package name needs deprecation period

Risks

  • Users may not notice migration (mitigated by deprecation warnings)
  • PyPI name llm-council availability (check before implementation)

References