iflow-mcp_modelcontextinterface-mcix 1.1.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/METADATA +931 -0
  2. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/RECORD +42 -0
  3. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/WHEEL +4 -0
  4. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/licenses/LICENSE +21 -0
  6. mci/__init__.py +10 -0
  7. mci/assets/example_toolset.mci.json +37 -0
  8. mci/assets/example_toolset.mci.yaml +23 -0
  9. mci/assets/gitignore +1 -0
  10. mci/assets/mci.json +29 -0
  11. mci/assets/mci.yaml +19 -0
  12. mci/cli/__init__.py +8 -0
  13. mci/cli/add.py +108 -0
  14. mci/cli/envs.py +257 -0
  15. mci/cli/formatters/__init__.py +12 -0
  16. mci/cli/formatters/env_formatter.py +83 -0
  17. mci/cli/formatters/json_formatter.py +93 -0
  18. mci/cli/formatters/table_formatter.py +138 -0
  19. mci/cli/formatters/yaml_formatter.py +93 -0
  20. mci/cli/install.py +147 -0
  21. mci/cli/list.py +153 -0
  22. mci/cli/run.py +125 -0
  23. mci/cli/validate.py +113 -0
  24. mci/core/__init__.py +8 -0
  25. mci/core/config.py +144 -0
  26. mci/core/dynamic_server.py +187 -0
  27. mci/core/file_finder.py +105 -0
  28. mci/core/mci_client.py +196 -0
  29. mci/core/mcp_server.py +240 -0
  30. mci/core/schema_editor.py +284 -0
  31. mci/core/tool_converter.py +119 -0
  32. mci/core/tool_manager.py +118 -0
  33. mci/core/validator.py +162 -0
  34. mci/mci.py +39 -0
  35. mci/py.typed +0 -0
  36. mci/utils/__init__.py +8 -0
  37. mci/utils/dotenv.py +170 -0
  38. mci/utils/env_scanner.py +84 -0
  39. mci/utils/error_formatter.py +165 -0
  40. mci/utils/error_handler.py +174 -0
  41. mci/utils/timestamp.py +50 -0
  42. mci/utils/validation.py +92 -0
mci/cli/run.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ run.py - Run command for starting MCP servers from MCI schemas
3
+
4
+ This module implements the `mcix run` command, which launches an MCP server
5
+ over STDIO that dynamically serves tools from an MCI schema file. The server
6
+ loads tools using MCIClient, converts them to MCP format, and delegates
7
+ execution back to MCIClient.
8
+ """
9
+
10
+ import asyncio
11
+ import os
12
+
13
+ import click
14
+ from mcipy import MCIClientError
15
+ from rich.console import Console
16
+
17
+ from mci.core.dynamic_server import run_server
18
+ from mci.core.file_finder import MCIFileFinder
19
+ from mci.utils.error_handler import ErrorHandler
20
+
21
+
22
+ @click.command()
23
+ @click.option(
24
+ "--file",
25
+ "-f",
26
+ type=click.Path(exists=True),
27
+ default=None,
28
+ help="Path to MCI schema file (defaults to mci.json or mci.yaml in current directory)",
29
+ )
30
+ @click.option(
31
+ "--filter",
32
+ type=str,
33
+ default=None,
34
+ help="Filter tools (format: type:value1,value2 - e.g., tags:api,database)",
35
+ )
36
+ def run(file: str | None, filter: str | None):
37
+ """
38
+ Run an MCP server that serves tools from an MCI schema.
39
+
40
+ Launches an MCP server over STDIO that dynamically loads and serves tools
41
+ from the specified MCI schema file. The server uses MCIClient to load tools
42
+ (including toolsets and environment variable templating), converts them to
43
+ MCP format, and delegates execution back to MCIClient.
44
+
45
+ The server supports:
46
+ - Loading tools from JSON/YAML MCI schemas
47
+ - Tool filtering by name, tags, and toolsets
48
+ - Environment variable templating
49
+ - Graceful shutdown on Ctrl+C
50
+
51
+ Examples:
52
+
53
+ # Run server with default mci.json or mci.yaml
54
+ mcix run
55
+
56
+ # Run server with specific file
57
+ mcix run --file=./custom.mci.json
58
+
59
+ # Run server with filtered tools
60
+ mcix run --filter=tags:api,database
61
+
62
+ # Run server with only specific tools
63
+ mcix run --filter=only:tool1,tool2
64
+
65
+ # Run server excluding specific tools
66
+ mcix run --filter=except:tool3,tool4
67
+
68
+ # Run server with tools from specific toolsets
69
+ mcix run --filter=toolsets:weather,database
70
+ """
71
+ console = Console()
72
+
73
+ try:
74
+ # Step 1: Find MCI file
75
+ if file is None:
76
+ finder = MCIFileFinder()
77
+ file = finder.find_mci_file()
78
+ if file is None:
79
+ console.print(
80
+ "[red]✗[/red] No MCI schema file found. "
81
+ "Run 'mcix install' to create one or specify --file.",
82
+ style="red",
83
+ )
84
+ raise click.Abort()
85
+
86
+ # Step 2: Validate filter spec if provided
87
+ if filter:
88
+ # Validate filter format early to provide better error messages
89
+ from mci.core.tool_manager import ToolManager
90
+
91
+ try:
92
+ ToolManager.parse_filter_spec(filter)
93
+ except ValueError as e:
94
+ console.print(f"[red]✗[/red] Invalid filter: {e}", style="red")
95
+ raise click.Abort() from e
96
+
97
+ # Step 3: Gather environment variables for templating
98
+ env_vars = dict(os.environ)
99
+
100
+ # Step 4: Display startup message
101
+ console.print("[green]⚡[/green] Starting MCP server...", style="bold green")
102
+ console.print(f"[cyan]📄 Schema:[/cyan] {file}")
103
+ if filter:
104
+ console.print(f"[cyan]🔍 Filter:[/cyan] {filter}")
105
+ console.print()
106
+ console.print("[dim]Press Ctrl+C to stop the server[/dim]")
107
+ console.print()
108
+
109
+ # Step 5: Run the server (this blocks until Ctrl+C)
110
+ asyncio.run(run_server(file, filter, env_vars))
111
+
112
+ except KeyboardInterrupt:
113
+ # Graceful shutdown on Ctrl+C
114
+ console.print()
115
+ console.print("[yellow]⏹[/yellow] Server stopped by user", style="yellow")
116
+ except click.Abort:
117
+ raise
118
+ except MCIClientError as e:
119
+ console.print()
120
+ console.print(ErrorHandler.format_mci_client_error(e))
121
+ raise click.Abort() from e
122
+ except Exception as e:
123
+ console.print()
124
+ console.print(ErrorHandler.format_generic_error(e))
125
+ raise click.Abort() from e
mci/cli/validate.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ validate.py - CLI command to validate MCI schemas
3
+
4
+ This module provides the `mcix validate` command which checks MCI schema
5
+ correctness using mci-py's built-in validation and provides user-friendly
6
+ error and warning messages.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+
12
+ import click
13
+
14
+ from mci.core.file_finder import MCIFileFinder
15
+ from mci.core.validator import MCIValidator
16
+ from mci.utils.error_formatter import ErrorFormatter
17
+
18
+
19
+ @click.command()
20
+ @click.option(
21
+ "--file",
22
+ "-f",
23
+ default=None,
24
+ help="Path to MCI schema file (default: auto-discover mci.json/mci.yaml)",
25
+ )
26
+ @click.option(
27
+ "--env",
28
+ "-e",
29
+ multiple=True,
30
+ help="Environment variables in KEY=VALUE format (can be specified multiple times)",
31
+ )
32
+ def validate(file: str | None, env: tuple[str, ...]) -> None:
33
+ """
34
+ Validate MCI schema file for correctness.
35
+
36
+ This command validates an MCI schema file using mci-py's built-in validation
37
+ engine. It checks for:
38
+ - Schema structure and syntax
39
+ - Required fields and data types
40
+ - Valid references and tool definitions
41
+
42
+ Additionally, it provides warnings for:
43
+ - Missing toolset files
44
+ - MCP commands not in PATH
45
+
46
+ Examples:
47
+ mcix validate # Validate default mci.json/mci.yaml
48
+ mcix validate --file custom.mci.json # Validate specific file
49
+ mcix validate -e API_KEY=123 # Provide environment variables
50
+ """
51
+ formatter = ErrorFormatter()
52
+
53
+ # Parse environment variables
54
+ env_vars = {}
55
+ for env_pair in env:
56
+ if "=" in env_pair:
57
+ key, value = env_pair.split("=", 1)
58
+ env_vars[key] = value
59
+ else:
60
+ formatter.console.print(
61
+ f"[yellow]Warning: Invalid environment variable format: {env_pair}. "
62
+ "Expected KEY=VALUE.[/yellow]"
63
+ )
64
+
65
+ # Merge environment variables: user-provided env vars override system environment variables
66
+ merged_env = {**os.environ, **env_vars}
67
+
68
+ # Find the schema file
69
+ if file is None:
70
+ file_finder = MCIFileFinder()
71
+ file = file_finder.find_mci_file()
72
+ if file is None:
73
+ formatter.console.print("[red]❌ No MCI schema file found in current directory[/red]\n")
74
+ formatter.console.print(
75
+ "[yellow]💡 Run 'mcix install' to create a default mci.json file[/yellow]"
76
+ )
77
+ sys.exit(1)
78
+
79
+ # Validate the schema
80
+ validator = MCIValidator(file_path=file, env_vars=merged_env)
81
+
82
+ try:
83
+ result = validator.validate_schema()
84
+
85
+ # Display errors
86
+ if result.errors:
87
+ formatter.format_validation_errors(result.errors)
88
+ formatter.console.print(
89
+ "\n[yellow]💡 Fix the errors above and run 'mcix validate' again[/yellow]\n"
90
+ )
91
+ sys.exit(1)
92
+
93
+ # Display warnings (if any)
94
+ if result.warnings:
95
+ formatter.format_validation_warnings(result.warnings)
96
+
97
+ # Display success message
98
+ formatter.format_validation_success(file)
99
+
100
+ # Exit with appropriate code
101
+ sys.exit(0)
102
+
103
+ except FileNotFoundError as e:
104
+ formatter.format_mci_error(f"File not found: {str(e)}")
105
+ sys.exit(1)
106
+ except Exception as e:
107
+ formatter.format_mci_error(f"Unexpected error: {str(e)}")
108
+ sys.exit(1)
109
+
110
+
111
+ # Allow running as script
112
+ if __name__ == "__main__":
113
+ validate()
mci/core/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Core business logic for the MCI CLI Tool.
3
+
4
+ This package contains the core functionality for MCI schema management,
5
+ tool loading, MCP server creation, and execution.
6
+ """
7
+
8
+ __all__ = ()
mci/core/config.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ config.py - Configuration loading and validation for MCI files
3
+
4
+ This module provides functionality to load and validate MCI configuration files
5
+ using the MCIClient from mci-py. It handles schema validation, error handling,
6
+ and provides user-friendly error messages. It also automatically loads environment
7
+ variables from .env files in the project root and ./mci directory.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from mcipy import MCIClient, MCIClientError
13
+
14
+ from mci.utils.dotenv import get_env_with_dotenv
15
+
16
+
17
+ class MCIConfig:
18
+ """
19
+ Manages MCI configuration file loading and validation.
20
+
21
+ This class provides methods to load MCI configuration files using the
22
+ MCIClient from mci-py, which performs built-in schema validation.
23
+ It also provides utilities for validating schemas and extracting
24
+ user-friendly error messages.
25
+
26
+ The class automatically loads environment variables from .env files in:
27
+ - The project root directory (same location as the MCI schema file)
28
+ - The ./mci directory
29
+
30
+ Variables are merged with project root taking precedence over ./mci/.env.
31
+ """
32
+
33
+ @staticmethod
34
+ def load(
35
+ file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
36
+ ) -> MCIClient:
37
+ """
38
+ Load and parse an MCI configuration file using MCIClient.
39
+
40
+ This method uses MCIClient from mci-py to load and validate the schema.
41
+ The MCIClient performs comprehensive schema validation during initialization.
42
+
43
+ If auto_load_dotenv is True (default), automatically loads environment variables
44
+ from .env and .env.mci files. Priority order:
45
+ - If .env.mci files exist:
46
+ 1. ./mci/.env.mci (library MCI-specific)
47
+ 2. Project root .env.mci (project MCI-specific)
48
+ - If no .env.mci files exist:
49
+ 1. ./mci/.env (library defaults)
50
+ 2. Project root .env (project-level)
51
+ - Then:
52
+ 3. System environment variables
53
+ 4. env_vars argument (highest priority)
54
+
55
+ Args:
56
+ file_path: Path to the MCI schema file (.json, .yaml, or .yml)
57
+ env_vars: Optional environment variables for template substitution (highest priority)
58
+ auto_load_dotenv: Whether to automatically load .env files (default: True)
59
+
60
+ Returns:
61
+ An initialized MCIClient instance
62
+
63
+ Raises:
64
+ MCIClientError: If the schema file cannot be loaded or parsed, or if
65
+ validation fails
66
+
67
+ Example:
68
+ >>> config = MCIConfig()
69
+ >>> try:
70
+ ... # Auto-loads .env files from project root and ./mci
71
+ ... client = config.load("mci.json")
72
+ ... tools = client.tools()
73
+ ... except MCIClientError as e:
74
+ ... print(f"Schema invalid: {e}")
75
+ """
76
+ try:
77
+ # Determine project root from schema file location
78
+ project_root = Path(file_path).parent.resolve()
79
+
80
+ # Load environment variables with proper precedence
81
+ if auto_load_dotenv:
82
+ merged_env = get_env_with_dotenv(project_root, env_vars)
83
+ else:
84
+ # If auto-loading is disabled, just use provided env_vars
85
+ merged_env = env_vars or {}
86
+
87
+ client = MCIClient(schema_file_path=file_path, env_vars=merged_env)
88
+ return client
89
+ except MCIClientError:
90
+ # Re-raise with the original error message from mci-py
91
+ raise
92
+
93
+ @staticmethod
94
+ def validate_schema(
95
+ file_path: str, env_vars: dict[str, str] | None = None, auto_load_dotenv: bool = True
96
+ ) -> tuple[bool, str]:
97
+ """
98
+ Validate an MCI schema file using MCIClient.
99
+
100
+ This method validates the schema using MCIClient in validation-only mode,
101
+ which skips template resolution for MCP servers and other runtime concerns.
102
+ MCIClient performs comprehensive validation including schema structure,
103
+ required fields, and data types.
104
+
105
+ If auto_load_dotenv is True (default), automatically loads environment variables
106
+ from .env files in the project root and ./mci directory.
107
+
108
+ Args:
109
+ file_path: Path to the MCI schema file to validate
110
+ env_vars: Optional environment variables for template substitution
111
+ auto_load_dotenv: Whether to automatically load .env files (default: True)
112
+
113
+ Returns:
114
+ A tuple of (is_valid, error_message) where:
115
+ - is_valid is True if the schema is valid, False otherwise
116
+ - error_message is empty string if valid, or contains error details if invalid
117
+
118
+ Example:
119
+ >>> config = MCIConfig()
120
+ >>> is_valid, error = config.validate_schema("mci.json")
121
+ >>> if not is_valid:
122
+ ... print(f"Validation failed: {error}")
123
+ """
124
+ try:
125
+ # Determine project root from schema file location
126
+ project_root = Path(file_path).parent.resolve()
127
+
128
+ # Load environment variables with proper precedence
129
+ if auto_load_dotenv:
130
+ merged_env = get_env_with_dotenv(project_root, env_vars)
131
+ else:
132
+ # If auto-loading is disabled, just use provided env_vars
133
+ merged_env = env_vars or {}
134
+
135
+ # Use validating=True to skip template resolution for MCP servers
136
+ # This allows validation without requiring all env_vars at validation time
137
+ MCIClient(schema_file_path=file_path, env_vars=merged_env, validating=True)
138
+ return (True, "")
139
+ except MCIClientError as e:
140
+ return (False, str(e))
141
+ except FileNotFoundError:
142
+ return (False, f"File not found: {file_path}")
143
+ except Exception as e:
144
+ return (False, f"Unexpected error: {str(e)}")
@@ -0,0 +1,187 @@
1
+ """
2
+ dynamic_server.py - Dynamic MCP server creation from MCI schemas
3
+
4
+ This module provides the DynamicMCPServer class for creating and running
5
+ MCP servers that dynamically load tools from MCI schema files. The server
6
+ uses MCIClient to load tools (including toolsets and environment variables),
7
+ converts them to MCP format, and delegates execution back to MCIClient.
8
+
9
+ The server supports:
10
+ - Loading tools from JSON/YAML MCI schemas
11
+ - Tool filtering by name, tags, and toolsets
12
+ - STDIO transport for MCP protocol
13
+ - Graceful shutdown and error handling
14
+ - Environment variable templating
15
+
16
+ Note: This module accesses dynamically added attributes on MCP Server instances
17
+ (prefixed with _mci_). Type checkers will report these as unknown attributes,
18
+ which is expected.
19
+ """
20
+ # pyright: reportAttributeAccessIssue=false
21
+
22
+ from mcp.server.lowlevel import Server
23
+
24
+ from mci.core.mci_client import MCIClientWrapper
25
+ from mci.core.mcp_server import MCPServerBuilder, ServerInstance
26
+ from mci.core.tool_manager import ToolManager
27
+
28
+
29
+ class DynamicMCPServer:
30
+ """
31
+ Creates and manages dynamic MCP servers from MCI schemas.
32
+
33
+ This class provides the main interface for creating MCP servers that serve
34
+ tools from MCI schema files. It handles loading, filtering, conversion,
35
+ and runtime management of the server.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ schema_path: str,
41
+ filter_spec: str | None = None,
42
+ env_vars: dict[str, str] | None = None,
43
+ ):
44
+ """
45
+ Initialize the dynamic server configuration.
46
+
47
+ Args:
48
+ schema_path: Path to the MCI schema file (.json, .yaml, or .yml)
49
+ filter_spec: Optional filter specification (e.g., "tags:api,database")
50
+ env_vars: Optional environment variables for template substitution
51
+ """
52
+ self.schema_path: str = schema_path
53
+ self.filter_spec: str | None = filter_spec
54
+ self.env_vars: dict[str, str] = env_vars or {}
55
+ self.server: Server | None = None
56
+ self.instance: ServerInstance | None = None
57
+ self.mci_client_wrapper: MCIClientWrapper | None = None
58
+
59
+ async def create_from_mci_schema(
60
+ self, server_name: str = "mci-dynamic-server", server_version: str = "1.0.0"
61
+ ) -> ServerInstance:
62
+ """
63
+ Create an MCP server instance from the MCI schema.
64
+
65
+ This method:
66
+ 1. Loads tools using MCIClient (with toolsets and env var templating)
67
+ 2. Applies filters if specified
68
+ 3. Converts tools to MCP format using Stage 8 converter
69
+ 4. Registers tools with MCP server
70
+ 5. Sets up handlers for tool listing and execution
71
+ 6. Returns ServerInstance ready for STDIO transport
72
+
73
+ Args:
74
+ server_name: Name for the MCP server
75
+ server_version: Version string for the server
76
+
77
+ Returns:
78
+ ServerInstance configured with MCI tools
79
+
80
+ Raises:
81
+ Exception: If schema loading, tool conversion, or server creation fails
82
+
83
+ Example:
84
+ >>> server = DynamicMCPServer("mci.json")
85
+ >>> instance = await server.create_from_mci_schema()
86
+ >>> await instance.start(stdio=True)
87
+ """
88
+ # Step 1: Load tools using MCIClient
89
+ self.mci_client_wrapper = MCIClientWrapper(self.schema_path, self.env_vars)
90
+
91
+ # Step 2: Apply filters if specified
92
+ if self.filter_spec:
93
+ tools = ToolManager.apply_filter_spec(self.mci_client_wrapper, self.filter_spec)
94
+ else:
95
+ tools = self.mci_client_wrapper.get_tools()
96
+
97
+ # Step 3: Create MCP server using Stage 8 infrastructure
98
+ builder = MCPServerBuilder(self.mci_client_wrapper.client)
99
+ self.server = await builder.create_server(server_name, server_version)
100
+
101
+ # Step 4: Register all tools (converter handles MCI to MCP conversion)
102
+ await builder.register_all_tools(self.server, tools)
103
+
104
+ # Step 5: Create ServerInstance (sets up handlers for listing and execution)
105
+ self.instance = ServerInstance(self.server, self.mci_client_wrapper.client, self.env_vars)
106
+
107
+ return self.instance
108
+
109
+ async def start_stdio(self) -> None:
110
+ """
111
+ Start the MCP server on STDIO transport.
112
+
113
+ This method starts the server and blocks until shutdown is requested.
114
+ The server will stop when:
115
+ - Ctrl+C (KeyboardInterrupt) is pressed
116
+ - The client disconnects
117
+ - An unrecoverable error occurs
118
+
119
+ The server will:
120
+ - Respond to MCP protocol requests for tool listing
121
+ - Execute tools by delegating to MCIClient.execute()
122
+ - Support structured output validation (outputSchema)
123
+ - Handle errors gracefully
124
+
125
+ Raises:
126
+ RuntimeError: If server instance is not created
127
+ Exception: If server startup or runtime fails
128
+
129
+ Example:
130
+ >>> server = DynamicMCPServer("mci.json")
131
+ >>> await server.create_from_mci_schema()
132
+ >>> await server.start_stdio() # Blocks until Ctrl+C
133
+ """
134
+ if self.instance is None:
135
+ raise RuntimeError("Server instance not created. Call create_from_mci_schema() first.")
136
+
137
+ try:
138
+ # Start the server on STDIO (this blocks until server stops)
139
+ # KeyboardInterrupt will be raised automatically when Ctrl+C is pressed
140
+ await self.instance.start(stdio=True)
141
+ except KeyboardInterrupt:
142
+ # Graceful shutdown on Ctrl+C
143
+ pass
144
+ finally:
145
+ # Cleanup
146
+ if self.instance:
147
+ self.instance.stop()
148
+
149
+ def get_tool_count(self) -> int:
150
+ """
151
+ Get the number of tools registered with the server.
152
+
153
+ Returns:
154
+ Number of registered tools, or 0 if server not created
155
+
156
+ Example:
157
+ >>> server = DynamicMCPServer("mci.json")
158
+ >>> await server.create_from_mci_schema()
159
+ >>> count = server.get_tool_count()
160
+ >>> print(f"Server has {count} tools")
161
+ """
162
+ if self.server and hasattr(self.server, "_mci_tools"):
163
+ return len(self.server._mci_tools) # type: ignore[attr-defined]
164
+ return 0
165
+
166
+
167
+ async def run_server(
168
+ schema_path: str, filter_spec: str | None = None, env_vars: dict[str, str] | None = None
169
+ ) -> None:
170
+ """
171
+ Create and run a dynamic MCP server from an MCI schema.
172
+
173
+ This is a convenience function that creates a server, loads tools,
174
+ and starts it on STDIO in a single call.
175
+
176
+ Args:
177
+ schema_path: Path to the MCI schema file
178
+ filter_spec: Optional filter specification (e.g., "tags:api,database")
179
+ env_vars: Optional environment variables for template substitution
180
+
181
+ Example:
182
+ >>> await run_server("mci.json")
183
+ >>> await run_server("mci.json", filter_spec="tags:api")
184
+ """
185
+ server = DynamicMCPServer(schema_path, filter_spec, env_vars)
186
+ await server.create_from_mci_schema()
187
+ await server.start_stdio()
@@ -0,0 +1,105 @@
1
+ """
2
+ file_finder.py - File discovery logic for MCI configuration files
3
+
4
+ This module provides functionality to locate MCI configuration files (mci.json or mci.yaml)
5
+ in a given directory. It prioritizes JSON files when both formats exist and provides
6
+ utilities for file format detection and validation.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from mci.utils.validation import file_exists
12
+
13
+
14
+ class MCIFileFinder:
15
+ """
16
+ Handles discovery of MCI configuration files in directories.
17
+
18
+ This class provides methods to find MCI configuration files (mci.json or mci.yaml),
19
+ with JSON files taking priority when both formats exist. It also includes utilities
20
+ for file validation and format detection.
21
+ """
22
+
23
+ @staticmethod
24
+ def find_mci_file(directory: str = ".") -> str | None:
25
+ """
26
+ Find an MCI configuration file in the specified directory.
27
+
28
+ Searches for mci.json first, then mci.yaml/mci.yml if JSON is not found.
29
+ This prioritizes JSON format when both formats exist in the same directory.
30
+
31
+ Args:
32
+ directory: The directory path to search in (default: current directory)
33
+
34
+ Returns:
35
+ The absolute path to the found MCI file, or None if no file is found
36
+
37
+ Example:
38
+ >>> finder = MCIFileFinder()
39
+ >>> path = finder.find_mci_file("./my_project")
40
+ >>> if path:
41
+ ... print(f"Found: {path}")
42
+ """
43
+ dir_path = Path(directory).resolve()
44
+
45
+ # Check for mci.json first (priority)
46
+ json_path = dir_path / "mci.json"
47
+ if json_path.exists() and json_path.is_file():
48
+ return str(json_path)
49
+
50
+ # Check for mci.yaml
51
+ yaml_path = dir_path / "mci.yaml"
52
+ if yaml_path.exists() and yaml_path.is_file():
53
+ return str(yaml_path)
54
+
55
+ # Check for mci.yml as well
56
+ yml_path = dir_path / "mci.yml"
57
+ if yml_path.exists() and yml_path.is_file():
58
+ return str(yml_path)
59
+
60
+ return None
61
+
62
+ @staticmethod
63
+ def validate_file_exists(path: str) -> bool:
64
+ """
65
+ Check if a file exists at the given path.
66
+
67
+ Delegates to the validation utility module to avoid code duplication.
68
+
69
+ Args:
70
+ path: The file path to validate
71
+
72
+ Returns:
73
+ True if the file exists and is a file, False otherwise
74
+
75
+ Example:
76
+ >>> finder = MCIFileFinder()
77
+ >>> exists = finder.validate_file_exists("./mci.json")
78
+ """
79
+ return file_exists(path)
80
+
81
+ @staticmethod
82
+ def get_file_format(path: str) -> str | None:
83
+ """
84
+ Determine the file format based on the file extension.
85
+
86
+ Args:
87
+ path: The file path to check
88
+
89
+ Returns:
90
+ "json" for .json files, "yaml" for .yaml/.yml files, None for unknown formats
91
+
92
+ Example:
93
+ >>> finder = MCIFileFinder()
94
+ >>> fmt = finder.get_file_format("./mci.json")
95
+ >>> print(fmt) # Output: "json"
96
+ """
97
+ file_path = Path(path)
98
+ ext = file_path.suffix.lower()
99
+
100
+ if ext == ".json":
101
+ return "json"
102
+ elif ext in [".yaml", ".yml"]:
103
+ return "yaml"
104
+ else:
105
+ return None