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.
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/METADATA +931 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/RECORD +42 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/WHEEL +4 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/entry_points.txt +2 -0
- iflow_mcp_modelcontextinterface_mcix-1.1.1.dev0.dist-info/licenses/LICENSE +21 -0
- mci/__init__.py +10 -0
- mci/assets/example_toolset.mci.json +37 -0
- mci/assets/example_toolset.mci.yaml +23 -0
- mci/assets/gitignore +1 -0
- mci/assets/mci.json +29 -0
- mci/assets/mci.yaml +19 -0
- mci/cli/__init__.py +8 -0
- mci/cli/add.py +108 -0
- mci/cli/envs.py +257 -0
- mci/cli/formatters/__init__.py +12 -0
- mci/cli/formatters/env_formatter.py +83 -0
- mci/cli/formatters/json_formatter.py +93 -0
- mci/cli/formatters/table_formatter.py +138 -0
- mci/cli/formatters/yaml_formatter.py +93 -0
- mci/cli/install.py +147 -0
- mci/cli/list.py +153 -0
- mci/cli/run.py +125 -0
- mci/cli/validate.py +113 -0
- mci/core/__init__.py +8 -0
- mci/core/config.py +144 -0
- mci/core/dynamic_server.py +187 -0
- mci/core/file_finder.py +105 -0
- mci/core/mci_client.py +196 -0
- mci/core/mcp_server.py +240 -0
- mci/core/schema_editor.py +284 -0
- mci/core/tool_converter.py +119 -0
- mci/core/tool_manager.py +118 -0
- mci/core/validator.py +162 -0
- mci/mci.py +39 -0
- mci/py.typed +0 -0
- mci/utils/__init__.py +8 -0
- mci/utils/dotenv.py +170 -0
- mci/utils/env_scanner.py +84 -0
- mci/utils/error_formatter.py +165 -0
- mci/utils/error_handler.py +174 -0
- mci/utils/timestamp.py +50 -0
- 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
|
+
)
|
mci/core/tool_manager.py
ADDED
|
@@ -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
|