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
@@ -0,0 +1,284 @@
1
+ """
2
+ schema_editor.py - Schema file editing logic
3
+
4
+ This module provides functionality to programmatically edit MCI schema files,
5
+ including adding toolset references with optional filters while preserving
6
+ the original file format (JSON or YAML).
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import yaml
14
+
15
+ from mci.core.file_finder import MCIFileFinder
16
+
17
+
18
+ class SchemaEditor:
19
+ """
20
+ Edits MCI schema files while preserving format and structure.
21
+
22
+ This class provides methods to load, modify, and save MCI schema files,
23
+ ensuring that the original file format (JSON or YAML) is preserved.
24
+ It supports adding toolset references with optional filters.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initialize the SchemaEditor with empty state."""
29
+ # Stores the loaded schema dictionary
30
+ self.schema_data: dict[str, Any] | None = None
31
+ # Stores the detected file format ('json' or 'yaml')
32
+ self.file_format: str | None = None
33
+ # Stores the path of the loaded schema file
34
+ self.file_path: str | None = None
35
+
36
+ def load_schema(self, file_path: str) -> dict[str, Any]:
37
+ """
38
+ Load an MCI schema file into memory.
39
+
40
+ Args:
41
+ file_path: Path to the MCI schema file (.json, .yaml, or .yml)
42
+
43
+ Returns:
44
+ The loaded schema data as a dictionary
45
+
46
+ Raises:
47
+ FileNotFoundError: If the file doesn't exist
48
+ ValueError: If the file format is unsupported
49
+ Exception: If the file cannot be parsed
50
+
51
+ Example:
52
+ >>> editor = SchemaEditor()
53
+ >>> schema = editor.load_schema("mci.json")
54
+ >>> print(schema.get("schemaVersion"))
55
+ """
56
+ path = Path(file_path)
57
+
58
+ if not path.exists():
59
+ raise FileNotFoundError(f"Schema file not found: {file_path}")
60
+
61
+ # Detect file format
62
+ file_finder = MCIFileFinder()
63
+ self.file_format = file_finder.get_file_format(str(path))
64
+
65
+ if self.file_format is None:
66
+ raise ValueError(f"Unsupported file format: {path.suffix}")
67
+
68
+ # Load the file
69
+ with open(path) as f:
70
+ if self.file_format == "json":
71
+ self.schema_data = json.load(f)
72
+ elif self.file_format == "yaml":
73
+ loaded_data = yaml.safe_load(f)
74
+ if loaded_data is None:
75
+ raise ValueError(f"YAML file is empty or invalid: {file_path}")
76
+ self.schema_data = loaded_data
77
+ else:
78
+ raise ValueError(f"Unsupported file format: {self.file_format}")
79
+
80
+ self.file_path = str(path)
81
+
82
+ # Type narrowing: at this point schema_data is guaranteed to be a dict
83
+ assert self.schema_data is not None
84
+ return self.schema_data
85
+
86
+ def add_toolset(
87
+ self, toolset_name: str, filter_type: str | None = None, filter_value: str | None = None
88
+ ) -> None:
89
+ """
90
+ Add a toolset reference to the schema.
91
+
92
+ This method adds a toolset to the schema's toolsets array. If the toolset
93
+ already exists, it will be updated with the new filter (if provided).
94
+
95
+ Args:
96
+ toolset_name: Name of the toolset to add
97
+ filter_type: Optional filter type (only, except, tags, withoutTags)
98
+ filter_value: Optional comma-separated filter values
99
+
100
+ Raises:
101
+ ValueError: If schema hasn't been loaded yet
102
+ ValueError: If filter_type is provided without filter_value or vice versa
103
+
104
+ Example:
105
+ >>> editor = SchemaEditor()
106
+ >>> editor.load_schema("mci.json")
107
+ >>> editor.add_toolset("weather-tools")
108
+ >>> editor.add_toolset("analytics", "only", "Tool1,Tool2")
109
+ >>> editor.save_schema("mci.json")
110
+ """
111
+ if self.schema_data is None:
112
+ raise ValueError("No schema loaded. Call load_schema() first.")
113
+
114
+ # Validate filter arguments
115
+ if (filter_type is None) != (filter_value is None):
116
+ raise ValueError("filter_type and filter_value must both be provided or both be None")
117
+
118
+ # Ensure toolsets array exists
119
+ if "toolsets" not in self.schema_data:
120
+ self.schema_data["toolsets"] = []
121
+
122
+ toolsets = self.schema_data["toolsets"]
123
+
124
+ # Check if toolset already exists
125
+ existing_index = None
126
+ for i, toolset in enumerate(toolsets):
127
+ # Toolsets can be strings or objects
128
+ if isinstance(toolset, str) and toolset == toolset_name:
129
+ existing_index = i
130
+ break
131
+ elif isinstance(toolset, dict) and toolset.get("name") == toolset_name:
132
+ existing_index = i
133
+ break
134
+
135
+ # Prepare the toolset entry
136
+ if filter_type and filter_value:
137
+ toolset_entry = {
138
+ "name": toolset_name,
139
+ "filter": filter_type,
140
+ "filterValue": filter_value,
141
+ }
142
+ else:
143
+ toolset_entry = toolset_name
144
+
145
+ # Add or update the toolset
146
+ if existing_index is not None:
147
+ toolsets[existing_index] = toolset_entry
148
+ else:
149
+ toolsets.append(toolset_entry)
150
+
151
+ def save_schema(self, file_path: str | None = None) -> None:
152
+ """
153
+ Save the schema back to a file, preserving the original format.
154
+
155
+ Args:
156
+ file_path: Optional path to save to. If None, uses the path from load_schema()
157
+
158
+ Raises:
159
+ ValueError: If no schema is loaded
160
+ ValueError: If file_path is None and no file was previously loaded
161
+ ValueError: If file format cannot be determined
162
+
163
+ Example:
164
+ >>> editor = SchemaEditor()
165
+ >>> editor.load_schema("mci.json")
166
+ >>> editor.add_toolset("weather-tools")
167
+ >>> editor.save_schema() # Saves to mci.json
168
+ """
169
+ if self.schema_data is None:
170
+ raise ValueError("No schema loaded. Call load_schema() first.")
171
+
172
+ # Determine the save path
173
+ save_path = file_path if file_path is not None else self.file_path
174
+
175
+ if save_path is None:
176
+ raise ValueError("No file path specified and no file was previously loaded")
177
+
178
+ # Determine format if saving to a new path
179
+ if file_path is not None:
180
+ file_finder = MCIFileFinder()
181
+ save_format = file_finder.get_file_format(file_path)
182
+ if save_format is None:
183
+ raise ValueError(f"Cannot determine format for file: {file_path}")
184
+ else:
185
+ save_format = self.file_format
186
+
187
+ if save_format is None:
188
+ raise ValueError("Cannot determine file format for saving")
189
+
190
+ # Write the file
191
+ path = Path(save_path)
192
+ with open(path, "w") as f:
193
+ if save_format == "json":
194
+ json.dump(self.schema_data, f, indent=2)
195
+ # Add newline at end of file for consistency
196
+ f.write("\n")
197
+ elif save_format == "yaml":
198
+ yaml.safe_dump(
199
+ self.schema_data,
200
+ f,
201
+ default_flow_style=False,
202
+ sort_keys=False,
203
+ allow_unicode=True,
204
+ )
205
+ else:
206
+ raise ValueError(f"Unsupported save format: {save_format}")
207
+
208
+ def preserve_format(self) -> str:
209
+ """
210
+ Get the format of the loaded schema file.
211
+
212
+ Returns:
213
+ The file format ("json" or "yaml")
214
+
215
+ Raises:
216
+ ValueError: If no schema has been loaded
217
+
218
+ Example:
219
+ >>> editor = SchemaEditor()
220
+ >>> editor.load_schema("mci.json")
221
+ >>> print(editor.preserve_format()) # Output: "json"
222
+ """
223
+ if self.file_format is None:
224
+ raise ValueError("No schema loaded. Call load_schema() first.")
225
+ return self.file_format
226
+
227
+
228
+ def parse_add_filter(filter_spec: str) -> tuple[str, str]:
229
+ """
230
+ Parse a filter specification string for the add command.
231
+
232
+ Filter specifications follow the format:
233
+ - "only:tool1,tool2,tool3" - Include only specified tools
234
+ - "except:tool1,tool2" - Exclude specified tools
235
+ - "tags:tag1,tag2" - Include tools with any of these tags
236
+ - "withoutTags:tag1,tag2" - Exclude tools with any of these tags
237
+
238
+ Args:
239
+ filter_spec: Filter specification string (e.g., "tags:api,database")
240
+
241
+ Returns:
242
+ Tuple of (filter_type, filter_value) where:
243
+ - filter_type is one of: "only", "except", "tags", "withoutTags"
244
+ - filter_value is the comma-separated list of values
245
+
246
+ Raises:
247
+ ValueError: If filter specification is invalid or malformed
248
+
249
+ Example:
250
+ >>> filter_type, filter_value = parse_add_filter("tags:api,database")
251
+ >>> print(filter_type, filter_value)
252
+ ('tags', 'api,database')
253
+ """
254
+ if not filter_spec or ":" not in filter_spec:
255
+ raise ValueError(
256
+ f"Invalid filter specification: '{filter_spec}'. "
257
+ "Expected format: 'type:value1,value2,...' "
258
+ "where type is one of: only, except, tags, withoutTags"
259
+ )
260
+
261
+ parts = filter_spec.split(":", 1)
262
+ if len(parts) != 2:
263
+ raise ValueError(f"Invalid filter specification: '{filter_spec}'")
264
+
265
+ filter_type = parts[0].strip()
266
+ filter_value = parts[1].strip()
267
+
268
+ # Validate filter type
269
+ valid_types = ["only", "except", "tags", "withoutTags"]
270
+ if filter_type not in valid_types:
271
+ raise ValueError(
272
+ f"Invalid filter type: '{filter_type}'. Valid types are: {', '.join(valid_types)}"
273
+ )
274
+
275
+ # Validate that we have values
276
+ if not filter_value:
277
+ raise ValueError(f"No values provided for filter type '{filter_type}'")
278
+
279
+ # Validate that values are not empty after splitting
280
+ values = [v.strip() for v in filter_value.split(",") if v.strip()]
281
+ if not values:
282
+ raise ValueError(f"No valid values found in filter specification: '{filter_spec}'")
283
+
284
+ return (filter_type, filter_value)
@@ -0,0 +1,119 @@
1
+ """
2
+ tool_converter.py - Convert MCI tools to MCP tool format
3
+
4
+ This module provides functionality to convert MCI tool definitions (from mci-py)
5
+ to MCP tool format compatible with the MCP protocol. It handles schema conversion,
6
+ metadata preservation, and ensures proper formatting for MCP servers.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ import mcp.types as types
12
+ from mcipy.models import Tool
13
+
14
+
15
+ class MCIToolConverter:
16
+ """
17
+ Converter for translating MCI tool definitions to MCP tool format.
18
+
19
+ This class handles the conversion of tool metadata, descriptions, and JSON schemas
20
+ from the MCI format (used by mci-py) to the MCP format required by MCP servers.
21
+ """
22
+
23
+ @staticmethod
24
+ def convert_to_mcp_tool(mci_tool: Tool) -> types.Tool:
25
+ """
26
+ Convert an MCI Tool to MCP Tool format.
27
+
28
+ Takes a Tool object from mci-py and converts it to the types.Tool format
29
+ expected by the MCP protocol. This includes converting the input schema,
30
+ preserving all metadata, and transferring annotations.
31
+
32
+ Args:
33
+ mci_tool: Tool object from mci-py (Pydantic model)
34
+
35
+ Returns:
36
+ types.Tool object compatible with MCP protocol
37
+
38
+ Example:
39
+ >>> converter = MCIToolConverter()
40
+ >>> mcp_tool = converter.convert_to_mcp_tool(mci_tool)
41
+ >>> print(mcp_tool.name, mcp_tool.description)
42
+ """
43
+ # Convert inputSchema to MCP format (JSON Schema)
44
+ input_schema = MCIToolConverter.convert_input_schema(mci_tool.inputSchema or {})
45
+
46
+ # Convert annotations to MCP format
47
+ annotations = MCIToolConverter.convert_annotations(mci_tool.annotations)
48
+
49
+ # Create MCP Tool with converted schema and annotations
50
+ return types.Tool(
51
+ name=mci_tool.name,
52
+ description=mci_tool.description or "",
53
+ inputSchema=input_schema,
54
+ annotations=annotations,
55
+ )
56
+
57
+ @staticmethod
58
+ def convert_input_schema(mci_schema: dict[str, Any]) -> dict[str, Any]:
59
+ """
60
+ Convert MCI inputSchema to MCP-compatible JSON Schema format.
61
+
62
+ MCI and MCP both use JSON Schema for input validation, but this method
63
+ ensures the schema is in the exact format expected by MCP servers.
64
+ Currently, both formats are compatible, so this is mostly a pass-through
65
+ with validation.
66
+
67
+ Args:
68
+ mci_schema: Input schema dictionary from MCI tool definition
69
+
70
+ Returns:
71
+ JSON Schema dictionary compatible with MCP protocol
72
+
73
+ Example:
74
+ >>> schema = {"type": "object", "properties": {"name": {"type": "string"}}}
75
+ >>> mcp_schema = MCIToolConverter.convert_input_schema(schema)
76
+ """
77
+ # Both MCI and MCP use JSON Schema, so we can pass through
78
+ # If the schema is empty, provide a minimal valid schema
79
+ if not mci_schema:
80
+ return {"type": "object", "properties": {}}
81
+
82
+ # Ensure the schema has at minimum a type field
83
+ if "type" not in mci_schema:
84
+ return {"type": "object", "properties": mci_schema}
85
+
86
+ return mci_schema
87
+
88
+ @staticmethod
89
+ def convert_annotations(mci_annotations: Any) -> types.ToolAnnotations | None:
90
+ """
91
+ Convert MCI Annotations to MCP ToolAnnotations format.
92
+
93
+ Transfers annotation fields from MCI tool annotations to the MCP format,
94
+ including title, readOnlyHint, destructiveHint, idempotentHint, and openWorldHint.
95
+
96
+ Args:
97
+ mci_annotations: Annotations object from MCI tool definition (or None)
98
+
99
+ Returns:
100
+ ToolAnnotations object compatible with MCP protocol, or None if no annotations
101
+
102
+ Example:
103
+ >>> from mcipy.models import Annotations
104
+ >>> mci_ann = Annotations(title="My Tool", readOnlyHint=True)
105
+ >>> mcp_ann = MCIToolConverter.convert_annotations(mci_ann)
106
+ >>> print(mcp_ann.title, mcp_ann.readOnlyHint)
107
+ """
108
+ if mci_annotations is None:
109
+ return None
110
+
111
+ # Convert MCI Annotations to MCP ToolAnnotations
112
+ # Both models have the same field structure, so we can extract and transfer
113
+ return types.ToolAnnotations(
114
+ title=mci_annotations.title,
115
+ readOnlyHint=mci_annotations.readOnlyHint,
116
+ destructiveHint=mci_annotations.destructiveHint,
117
+ idempotentHint=mci_annotations.idempotentHint,
118
+ openWorldHint=mci_annotations.openWorldHint,
119
+ )
@@ -0,0 +1,118 @@
1
+ """
2
+ tool_manager.py - Tool filtering and management logic
3
+
4
+ This module provides utilities for parsing filter specifications from the CLI
5
+ and applying them to tools using the MCIClient filtering methods.
6
+ """
7
+
8
+ from mcipy.models import Tool
9
+
10
+ from mci.core.mci_client import MCIClientWrapper
11
+
12
+
13
+ class ToolManager:
14
+ """
15
+ Manages tool filtering based on CLI filter specifications.
16
+
17
+ This class parses filter specifications from command-line arguments
18
+ and applies them using MCIClient's built-in filtering methods.
19
+ """
20
+
21
+ @staticmethod
22
+ def parse_filter_spec(filter_spec: str) -> tuple[str, list[str]]:
23
+ """
24
+ Parse a filter specification string into filter type and values.
25
+
26
+ Filter specifications follow the format:
27
+ - "only:tool1,tool2,tool3" - Include only specified tools
28
+ - "except:tool1,tool2" - Exclude specified tools
29
+ - "tags:tag1,tag2" - Include tools with any of these tags
30
+ - "without-tags:tag1,tag2" - Exclude tools with any of these tags
31
+ - "toolsets:toolset1,toolset2" - Include tools from specified toolsets
32
+
33
+ Args:
34
+ filter_spec: Filter specification string (e.g., "tags:api,database")
35
+
36
+ Returns:
37
+ Tuple of (filter_type, values) where:
38
+ - filter_type is one of: "only", "except", "tags", "without-tags", "toolsets"
39
+ - values is a list of filter values
40
+
41
+ Raises:
42
+ ValueError: If filter specification is invalid or malformed
43
+
44
+ Example:
45
+ >>> filter_type, values = ToolManager.parse_filter_spec("tags:api,database")
46
+ >>> print(filter_type, values)
47
+ ('tags', ['api', 'database'])
48
+ """
49
+ if not filter_spec or ":" not in filter_spec:
50
+ raise ValueError(
51
+ f"Invalid filter specification: '{filter_spec}'. "
52
+ "Expected format: 'type:value1,value2,...' "
53
+ "where type is one of: only, except, tags, without-tags, toolsets"
54
+ )
55
+
56
+ parts = filter_spec.split(":", 1)
57
+ if len(parts) != 2:
58
+ raise ValueError(f"Invalid filter specification: '{filter_spec}'")
59
+
60
+ filter_type = parts[0].strip()
61
+ values_str = parts[1].strip()
62
+
63
+ # Validate filter type
64
+ valid_types = ["only", "except", "tags", "without-tags", "toolsets"]
65
+ if filter_type not in valid_types:
66
+ raise ValueError(
67
+ f"Invalid filter type: '{filter_type}'. Valid types are: {', '.join(valid_types)}"
68
+ )
69
+
70
+ # Parse comma-separated values
71
+ if not values_str:
72
+ raise ValueError(f"No values provided for filter type '{filter_type}'")
73
+
74
+ values = [v.strip() for v in values_str.split(",") if v.strip()]
75
+ if not values:
76
+ raise ValueError(f"No valid values found in filter specification: '{filter_spec}'")
77
+
78
+ return (filter_type, values)
79
+
80
+ @staticmethod
81
+ def apply_filter_spec(client: MCIClientWrapper, filter_spec: str) -> list[Tool]:
82
+ """
83
+ Apply a filter specification to get filtered tools.
84
+
85
+ This method parses the filter specification and applies the appropriate
86
+ MCIClient filtering method.
87
+
88
+ Args:
89
+ client: MCIClientWrapper instance to apply filters on
90
+ filter_spec: Filter specification string (e.g., "tags:api,database")
91
+
92
+ Returns:
93
+ Filtered list of Tool objects
94
+
95
+ Raises:
96
+ ValueError: If filter specification is invalid
97
+
98
+ Example:
99
+ >>> wrapper = MCIClientWrapper("mci.json")
100
+ >>> tools = ToolManager.apply_filter_spec(wrapper, "tags:api,database")
101
+ >>> print([t.name for t in tools])
102
+ """
103
+ filter_type, values = ToolManager.parse_filter_spec(filter_spec)
104
+
105
+ # Apply the appropriate filter based on type
106
+ if filter_type == "only":
107
+ return client.filter_only(values)
108
+ elif filter_type == "except":
109
+ return client.filter_except(values)
110
+ elif filter_type == "tags":
111
+ return client.filter_tags(values)
112
+ elif filter_type == "without-tags":
113
+ return client.filter_without_tags(values)
114
+ elif filter_type == "toolsets":
115
+ return client.filter_toolsets(values)
116
+ else:
117
+ # This shouldn't happen if parse_filter_spec validates correctly
118
+ raise ValueError(f"Unsupported filter type: '{filter_type}'")
mci/core/validator.py ADDED
@@ -0,0 +1,162 @@
1
+ """
2
+ validator.py - Schema validation logic using mci-py
3
+
4
+ This module provides validation functionality for MCI schemas,
5
+ leveraging mci-py's built-in MCIClient validation and adding
6
+ additional checks for toolset files and MCP command availability.
7
+ """
8
+
9
+ import json
10
+ import shutil
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ from mci.core.config import MCIConfig
18
+ from mci.utils.error_formatter import ValidationError, ValidationWarning
19
+
20
+
21
+ @dataclass
22
+ class ValidationResult:
23
+ """
24
+ Result of validating an MCI schema.
25
+
26
+ Attributes:
27
+ errors: List of validation errors from MCIClient
28
+ warnings: List of validation warnings from additional checks
29
+ is_valid: True if no errors were found (warnings are OK)
30
+ """
31
+
32
+ errors: list[ValidationError]
33
+ warnings: list[ValidationWarning]
34
+ is_valid: bool
35
+
36
+
37
+ class MCIValidator:
38
+ """
39
+ Validates MCI schemas using MCIClient and performs additional checks.
40
+
41
+ This class uses mci-py's built-in validation via MCIClient and adds
42
+ extra validation for toolset file existence and MCP command availability.
43
+ """
44
+
45
+ def __init__(self, file_path: str, env_vars: dict[str, str] | None = None):
46
+ """
47
+ Initialize the validator.
48
+
49
+ Args:
50
+ file_path: Path to the MCI schema file to validate
51
+ env_vars: Optional environment variables for template substitution
52
+ """
53
+ self.file_path: str = file_path
54
+ self.env_vars: dict[str, str] = env_vars or {}
55
+ self.schema_data: dict[str, Any] | None = None
56
+
57
+ def validate_schema(self) -> ValidationResult:
58
+ """
59
+ Validate the MCI schema file using MCIClient.
60
+
61
+ This method uses MCIConfig.validate_schema which wraps MCIClient
62
+ validation. It then performs additional checks for toolset files
63
+ and MCP commands.
64
+
65
+ Returns:
66
+ ValidationResult with errors, warnings, and validity status
67
+
68
+ Example:
69
+ >>> validator = MCIValidator("mci.json")
70
+ >>> result = validator.validate_schema()
71
+ >>> if not result.is_valid:
72
+ ... print("Schema has errors!")
73
+ """
74
+ errors: list[ValidationError] = []
75
+ warnings: list[ValidationWarning] = []
76
+
77
+ # First, validate using MCIClient (primary validation)
78
+ config = MCIConfig()
79
+ is_valid, error_message = config.validate_schema(self.file_path, self.env_vars)
80
+
81
+ if not is_valid:
82
+ # Parse the error message from MCIClient
83
+ errors.append(ValidationError(message=error_message))
84
+ # If schema is invalid, we can't perform additional checks
85
+ return ValidationResult(errors=errors, warnings=warnings, is_valid=False)
86
+
87
+ # Load schema data for additional checks
88
+ try:
89
+ self._load_schema_data()
90
+ except Exception as e:
91
+ errors.append(ValidationError(message=f"Failed to load schema data: {str(e)}"))
92
+ return ValidationResult(errors=errors, warnings=warnings, is_valid=False)
93
+
94
+ # Perform additional checks for MCP commands (as warnings)
95
+ mcp_warnings = self.check_mcp_commands()
96
+ warnings.extend(mcp_warnings)
97
+
98
+ return ValidationResult(errors=errors, warnings=warnings, is_valid=True)
99
+
100
+ def _load_schema_data(self) -> None:
101
+ """
102
+ Load the raw schema data from the file.
103
+
104
+ This is used for additional validation checks beyond what MCIClient provides.
105
+
106
+ Raises:
107
+ Exception: If the file cannot be loaded or parsed
108
+ """
109
+ file_path = Path(self.file_path)
110
+
111
+ if not file_path.exists():
112
+ raise FileNotFoundError(f"Schema file not found: {self.file_path}")
113
+
114
+ with open(file_path) as f:
115
+ if file_path.suffix == ".json":
116
+ self.schema_data = json.load(f)
117
+ elif file_path.suffix in [".yaml", ".yml"]:
118
+ self.schema_data = yaml.safe_load(f)
119
+ else:
120
+ raise ValueError(f"Unsupported file format: {file_path.suffix}")
121
+
122
+ def check_mcp_commands(self) -> list[ValidationWarning]:
123
+ """
124
+ Check that MCP server commands are available in PATH.
125
+
126
+ Returns:
127
+ List of ValidationWarning for missing MCP commands
128
+
129
+ Example:
130
+ >>> validator = MCIValidator("mci.json")
131
+ >>> validator._load_schema_data()
132
+ >>> warnings = validator.check_mcp_commands()
133
+ """
134
+ warnings: list[ValidationWarning] = []
135
+
136
+ if not self.schema_data:
137
+ return warnings
138
+
139
+ mcp_servers = self.schema_data.get("mcp_servers", {})
140
+ if not mcp_servers:
141
+ return warnings
142
+
143
+ # Type narrowing: guard against invalid schema data where mcp_servers
144
+ # might not be a dict (in case it passed MCIClient validation but has unexpected type)
145
+ if not isinstance(mcp_servers, dict):
146
+ return warnings
147
+
148
+ for server_name, server_config in mcp_servers.items():
149
+ if isinstance(server_config, dict):
150
+ command = server_config.get("command")
151
+ if command:
152
+ # Check if command is available in PATH
153
+ # Ensure command is a string (not PathLike) to avoid deprecation warning
154
+ if not shutil.which(str(command)):
155
+ warnings.append(
156
+ ValidationWarning(
157
+ message=f"MCP server command not found in PATH: {command} (server: {server_name})",
158
+ suggestion="Install the command or ensure it's in your PATH",
159
+ )
160
+ )
161
+
162
+ return warnings