This document describes the technical architecture and design decisions for the MCI CLI tool.
The MCI CLI tool is built on a modular architecture that separates concerns into distinct layers:
- CLI Layer - Command-line interface using Click
- Core Layer - Business logic and MCI/MCP integration
- Utils Layer - Shared utilities and helpers
All tool loading, filtering, and schema operations are delegated to MCIClient from mci-py. The CLI tool acts as a thin wrapper that:
- Provides command-line interface to mci-py functionality
- Formats output for terminal display
- Handles file discovery and user interaction
- Manages MCP server lifecycle
This ensures consistency with the upstream mci-py adapter and avoids reimplementing core logic.
Each command is implemented as a separate module in src/mci/cli/, making it easy to:
- Add new commands
- Test commands in isolation
- Maintain consistent command structure
- Share common utilities
The CLI provides user-friendly error messages by:
- Catching
MCIClientErrorexceptions from mci-py - Extracting error details and context
- Formatting with Rich library for visual clarity
- Providing actionable suggestions for resolution
All commands follow a consistent pattern:
import click
from mci.core.config import MCIConfig
from mci.utils.error_handler import ErrorHandler
@click.command()
@click.option("--file", "-f", help="Path to MCI file")
def command_name(file: str | None):
"""Command description."""
try:
# Load configuration
config = MCIConfig()
client = config.load(file or "mci.json")
# Execute command logic
result = do_something(client)
# Display results
print(result)
except MCIClientError as e:
# Handle errors
click.echo(ErrorHandler.format_mci_client_error(e))
raise click.Abort()Output formatters (src/mci/cli/formatters/) handle different output formats:
- TableFormatter: Rich-based terminal tables
- JSONFormatter: JSON output with timestamps
- YAMLFormatter: YAML output with timestamps
All formatters implement a common interface for consistency.
MCIConfig handles configuration loading:
class MCIConfig:
def load(self, file_path: str, env_vars: dict = None) -> MCIClient:
"""Load and validate MCI configuration."""
# Uses MCIClient from mci-py
def validate_schema(self, file_path: str) -> tuple[bool, str]:
"""Validate schema without loading."""MCIFileFinder discovers MCI files:
class MCIFileFinder:
def find_mci_file(self, directory: str) -> str | None:
"""Find mci.json or mci.yaml in directory."""
def get_file_format(self, file_path: str) -> str:
"""Detect file format (json/yaml)."""MCIClientWrapper wraps MCIClient for CLI use:
class MCIClientWrapper:
def __init__(self, schema_path: str, env_vars: dict = None):
self.client = MCIClient(schema_file_path=schema_path, env_vars=env_vars)
def get_tools(self) -> list:
"""Get all tools from schema."""
def filter_tags(self, tags: list[str]) -> list:
"""Filter tools by tags."""ToolManager parses and applies filter specifications:
class ToolManager:
@staticmethod
def apply_filter_spec(wrapper: MCIClientWrapper, spec: str) -> list:
"""Parse and apply filter specification."""
# Supports: tags:, only:, except:, without-tags:, toolsets:MCIToolConverter converts MCI tools to MCP format:
class MCIToolConverter:
@staticmethod
def to_mcp_tool(mci_tool) -> types.Tool:
"""Convert MCI tool to MCP Tool."""MCPServerBuilder creates MCP servers:
class MCPServerBuilder:
async def create_server(self, name: str, version: str) -> Server:
"""Create MCP server instance."""
async def register_all_tools(self, server: Server, tools: list):
"""Register tools with server."""ServerInstance manages server lifecycle:
class ServerInstance:
async def start(self, stdio: bool = True):
"""Start server on STDIO or other transport."""
def stop(self):
"""Stop server gracefully."""DynamicMCPServer provides high-level server creation:
class DynamicMCPServer:
async def create_from_mci_schema(self, server_name: str) -> ServerInstance:
"""Create server from MCI schema with filtering."""
async def start_stdio(self):
"""Start server on STDIO."""SchemaEditor modifies MCI schema files:
class SchemaEditor:
def add_toolset(self, toolset_name: str, filter_spec: str = None):
"""Add toolset reference to schema."""
def save(self):
"""Save changes preserving format."""ErrorHandler formats MCIClient errors:
class ErrorHandler:
@staticmethod
def format_mci_client_error(error: MCIClientError) -> str:
"""Format error with suggestions."""ErrorFormatter provides Rich-based display:
class ErrorFormatter:
def format_validation_errors(self, errors: list[ValidationError]):
"""Display validation errors in panel."""
def format_validation_success(self, file_path: str):
"""Display success message."""Validation utilities for file operations:
def is_valid_path(path: str) -> bool:
"""Check if path is valid."""
def file_exists(path: str) -> bool:
"""Check if file exists."""
def is_readable(path: str) -> bool:
"""Check if file is readable."""User Command
↓
MCIConfig.load()
↓
MCIClient (mci-py)
↓
Parse schema → Load toolsets → Apply environment variables
↓
Return MCIClient instance
↓
MCIClientWrapper
↓
Apply filters (if specified)
↓
Return filtered tools
User runs `mcix run`
↓
Load MCI schema via MCIConfig
↓
Create MCIClientWrapper
↓
Apply filter_spec if provided
↓
Create DynamicMCPServer
↓
MCPServerBuilder.create_server()
↓
Convert tools: MCI → MCP (MCIToolConverter)
↓
Register tools with server
↓
Create ServerInstance
↓
Start STDIO transport
↓
Handle MCP protocol requests:
- tools/list → Return registered tools
- tools/call → Delegate to MCIClient.execute()
MCP client sends tools/call request
↓
ServerInstance receives request
↓
Extract tool name and arguments
↓
MCIClient.execute(tool_name, properties)
↓
MCIClient handles execution:
- Load tool definition
- Apply templating
- Execute based on type (text/file/cli/http/mcp)
- Return result
↓
ServerInstance formats result as MCP response
↓
Return to client
- Consistency: Ensures CLI and programmatic usage behave identically
- Maintainability: Core logic maintained in one place
- Reliability: mci-py is well-tested and handles edge cases
- Future-proof: New mci-py features automatically available
- User Experience: Beautiful terminal output with colors and formatting
- Accessibility: Fallback to plain text when colors not supported
- Consistency: Professional look across all commands
- Maintainability: Simple API for complex output
- Pythonic: Natural Python API for commands and options
- Powerful: Supports complex argument parsing and validation
- Extensible: Easy to add new commands
- Well-documented: Extensive documentation and examples
- Flexibility: Users choose output format (table/json/yaml)
- Testability: Easy to test formatting logic
- Reusability: Formatters can be used across commands
- Maintainability: Changes to one format don't affect others
- Schemas are loaded once per command execution
- MCIClient caches parsed tools
- Toolset files loaded lazily by MCIClient
- Filtering done in-memory on already-loaded tools
- O(n) complexity where n = number of tools
- Acceptable for typical tool counts (< 1000)
- Tools converted to MCP format once at startup
- Kept in memory for fast access
- Tool execution delegates to MCIClient (no caching)
- Path validation prevents directory traversal
- Toolset files must be in
./mci/directory - File operations use Path library for safety
- CLI execution delegated to MCIClient
- MCIClient handles sandboxing and validation
- No shell injection possible in MCI CLI layer
- Environment variables explicitly passed to MCIClient
- No automatic injection of sensitive variables
- User controls which variables are available
- Create new file in
src/mci/cli/ - Implement Click command
- Import in
src/mci/mci.py - Add command to CLI group
- Write tests in
tests/unit/cli/
- Create formatter in
src/mci/cli/formatters/ - Implement common interface
- Register in commands that support formats
- Write tests in
tests/unit/cli/formatters/
- Add validation logic to
src/mci/core/validator.py - Update
validatecommand to use new validators - Add error/warning formatting
- Write tests in
tests/unit/core/
- Test individual functions/classes in isolation
- Mock external dependencies (file system, MCIClient)
- Cover edge cases and error conditions
- Fast execution (< 1s per test)
- Test complete command workflows
- Use real files and configurations
- Verify output format and content
- Slower but more comprehensive
- Test complex integration scenarios
- Verify MCP server behavior
- Test with real MCP clients
- Human-verified output
- Caching: Cache parsed schemas for faster repeated commands
- Parallel Loading: Load toolset files in parallel
- Streaming: Support streaming output for large tool lists
- Plugins: Plugin system for custom commands/formatters
- Config Profiles: Support multiple configuration profiles
- Large Schemas: May be slow with 1000+ tools (not optimized)
- Binary Output: No support for binary tool outputs
- Concurrent Execution: One tool execution at a time per server
- Error Recovery: Limited error recovery in MCP server
For more information, see development.md and PLAN.md.