fid-mcp 0.1.5__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.
- fid_mcp/__init__.py +1 -0
- fid_mcp/config.py +243 -0
- fid_mcp/server.py +611 -0
- fid_mcp/shell.py +883 -0
- fid_mcp-0.1.5.dist-info/METADATA +29 -0
- fid_mcp-0.1.5.dist-info/RECORD +9 -0
- fid_mcp-0.1.5.dist-info/WHEEL +4 -0
- fid_mcp-0.1.5.dist-info/entry_points.txt +2 -0
- fid_mcp-0.1.5.dist-info/licenses/LICENSE +6 -0
fid_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# fid-mcp package
|
fid_mcp/config.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Any, List, TypeVar, Type, cast, Callable
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
EnumT = TypeVar("EnumT", bound=Enum)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def from_str(x: Any) -> str:
|
|
14
|
+
assert isinstance(x, str)
|
|
15
|
+
return x
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_int(x: Any) -> int:
|
|
19
|
+
assert isinstance(x, int) and not isinstance(x, bool)
|
|
20
|
+
return x
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def to_enum(c: Type[EnumT], x: Any) -> EnumT:
|
|
24
|
+
assert isinstance(x, c)
|
|
25
|
+
return x.value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_class(c: Type[T], x: Any) -> dict:
|
|
29
|
+
assert isinstance(x, c)
|
|
30
|
+
return cast(Any, x).to_dict()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
|
|
34
|
+
assert isinstance(x, list)
|
|
35
|
+
return [f(y) for y in x]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Function(Enum):
|
|
39
|
+
"""The type of function to execute"""
|
|
40
|
+
|
|
41
|
+
SHELL = "shell"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ShellParams(BaseModel):
|
|
45
|
+
command: str
|
|
46
|
+
wait: int
|
|
47
|
+
"""Wait time in seconds"""
|
|
48
|
+
expect_patterns: List[str] | None = None
|
|
49
|
+
"""Regex patterns to expect during command execution"""
|
|
50
|
+
responses: List[str] | None = None
|
|
51
|
+
"""Responses to send when patterns are matched (must match length of expect_patterns)"""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def from_dict(obj: Any) -> 'ShellParams':
|
|
55
|
+
assert isinstance(obj, dict)
|
|
56
|
+
command = from_str(obj.get("command"))
|
|
57
|
+
wait = from_int(obj.get("wait"))
|
|
58
|
+
expect_patterns = from_list(from_str, obj.get("expect_patterns")) if obj.get("expect_patterns") else None
|
|
59
|
+
responses = from_list(from_str, obj.get("responses")) if obj.get("responses") else None
|
|
60
|
+
return ShellParams(command=command, wait=wait, expect_patterns=expect_patterns, responses=responses)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
result: dict = {}
|
|
64
|
+
result["command"] = from_str(self.command)
|
|
65
|
+
result["wait"] = from_int(self.wait)
|
|
66
|
+
if self.expect_patterns is not None:
|
|
67
|
+
result["expect_patterns"] = from_list(from_str, self.expect_patterns)
|
|
68
|
+
if self.responses is not None:
|
|
69
|
+
result["responses"] = from_list(from_str, self.responses)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Step(BaseModel):
|
|
74
|
+
function: Function
|
|
75
|
+
"""The type of function to execute"""
|
|
76
|
+
|
|
77
|
+
shell_params: ShellParams
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def from_dict(obj: Any) -> 'Step':
|
|
81
|
+
assert isinstance(obj, dict)
|
|
82
|
+
function = Function(obj.get("function"))
|
|
83
|
+
shell_params = ShellParams.from_dict(obj.get("shellParams"))
|
|
84
|
+
return Step(function=function, shell_params=shell_params)
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> dict:
|
|
87
|
+
result: dict = {}
|
|
88
|
+
result["function"] = to_enum(Function, self.function)
|
|
89
|
+
result["shellParams"] = to_class(ShellParams, self.shell_params)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Param(BaseModel):
|
|
94
|
+
default: str
|
|
95
|
+
name: str
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def from_dict(obj: Any) -> 'Param':
|
|
99
|
+
assert isinstance(obj, dict)
|
|
100
|
+
default = from_str(obj.get("default"))
|
|
101
|
+
name = from_str(obj.get("name"))
|
|
102
|
+
return Param(default=default, name=name)
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict:
|
|
105
|
+
result: dict = {}
|
|
106
|
+
result["default"] = from_str(self.default)
|
|
107
|
+
result["name"] = from_str(self.name)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Tool(BaseModel):
|
|
112
|
+
description: str
|
|
113
|
+
name: str
|
|
114
|
+
steps: List[Step]
|
|
115
|
+
tool_params: List[Param]
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def from_dict(obj: Any) -> 'Tool':
|
|
119
|
+
assert isinstance(obj, dict)
|
|
120
|
+
description = from_str(obj.get("description"))
|
|
121
|
+
name = from_str(obj.get("name"))
|
|
122
|
+
steps = from_list(Step.from_dict, obj.get("steps"))
|
|
123
|
+
tool_params = from_list(Param.from_dict, obj.get("toolParams"))
|
|
124
|
+
return Tool(description=description, name=name, steps=steps, tool_params=tool_params)
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict:
|
|
127
|
+
result: dict = {}
|
|
128
|
+
result["description"] = from_str(self.description)
|
|
129
|
+
result["name"] = from_str(self.name)
|
|
130
|
+
result["steps"] = from_list(lambda x: to_class(Step, x), self.steps)
|
|
131
|
+
result["toolParams"] = from_list(lambda x: to_class(Param, x), self.tool_params)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Version(Enum):
|
|
136
|
+
THE_100 = "1.0.0"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Coordinate(BaseModel):
|
|
140
|
+
description: str
|
|
141
|
+
name: str
|
|
142
|
+
tools: List[Tool]
|
|
143
|
+
version: Version
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def from_dict(obj: Any) -> 'Coordinate':
|
|
147
|
+
assert isinstance(obj, dict)
|
|
148
|
+
description = from_str(obj.get("description"))
|
|
149
|
+
name = from_str(obj.get("name"))
|
|
150
|
+
tools = from_list(Tool.from_dict, obj.get("tools"))
|
|
151
|
+
version = Version(obj.get("version"))
|
|
152
|
+
return Coordinate(description=description, name=name, tools=tools, version=version)
|
|
153
|
+
|
|
154
|
+
def to_dict(self) -> dict:
|
|
155
|
+
result: dict = {}
|
|
156
|
+
result["description"] = from_str(self.description)
|
|
157
|
+
result["name"] = from_str(self.name)
|
|
158
|
+
result["tools"] = from_list(lambda x: to_class(Tool, x), self.tools)
|
|
159
|
+
result["version"] = to_enum(Version, self.version)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def coordinate_from_dict(s: Any) -> Coordinate:
|
|
164
|
+
return Coordinate.from_dict(s)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def coordinate_to_dict(x: Coordinate) -> Any:
|
|
168
|
+
return to_class(Coordinate, x)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_and_validate_config(config_path: str) -> Coordinate:
|
|
172
|
+
"""Load and validate a fidtools configuration file using Pydantic models"""
|
|
173
|
+
config_path = Path(config_path)
|
|
174
|
+
|
|
175
|
+
# Load the configuration file
|
|
176
|
+
with open(config_path, 'r') as f:
|
|
177
|
+
config_data = json.load(f)
|
|
178
|
+
|
|
179
|
+
# Validate using Pydantic models and additional validation
|
|
180
|
+
validate_config_dict(config_data)
|
|
181
|
+
|
|
182
|
+
# Convert to Coordinate object
|
|
183
|
+
return coordinate_from_dict(config_data)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def validate_config_dict(config_data: dict) -> None:
|
|
187
|
+
"""Validate a configuration dictionary using Pydantic models and check parameter references"""
|
|
188
|
+
# Validate using Pydantic by attempting to create a Coordinate object
|
|
189
|
+
try:
|
|
190
|
+
coordinate_from_dict(config_data)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise ValueError(f"Configuration validation failed: {str(e)}")
|
|
193
|
+
|
|
194
|
+
# Additional validation: check parameter references
|
|
195
|
+
_validate_parameter_references(config_data)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _validate_parameter_references(config_data: dict) -> None:
|
|
199
|
+
"""Validate that all parameter references in shellParams are defined in toolParams"""
|
|
200
|
+
for tool_idx, tool in enumerate(config_data.get("tools", [])):
|
|
201
|
+
tool_name = tool.get("name", f"tool[{tool_idx}]")
|
|
202
|
+
|
|
203
|
+
# Get defined parameters
|
|
204
|
+
defined_params = set()
|
|
205
|
+
for param in tool.get("toolParams", []):
|
|
206
|
+
defined_params.add(param.get("name"))
|
|
207
|
+
|
|
208
|
+
# Check each step for parameter references
|
|
209
|
+
for step_idx, step in enumerate(tool.get("steps", [])):
|
|
210
|
+
if step.get("function") == "shell" and "shellParams" in step:
|
|
211
|
+
shell_params = step["shellParams"]
|
|
212
|
+
|
|
213
|
+
# Check parameter references in command
|
|
214
|
+
if "command" in shell_params:
|
|
215
|
+
referenced_params = _extract_parameter_references(shell_params["command"])
|
|
216
|
+
|
|
217
|
+
# Check if all referenced parameters are defined
|
|
218
|
+
undefined_params = referenced_params - defined_params
|
|
219
|
+
if undefined_params:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Tool '{tool_name}' step {step_idx}: "
|
|
222
|
+
f"shellParams.command references undefined parameters: {sorted(undefined_params)}. "
|
|
223
|
+
f"Defined parameters: {sorted(defined_params)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _extract_parameter_references(text: str) -> set:
|
|
228
|
+
"""Extract parameter references (${param}) from a text string"""
|
|
229
|
+
if not isinstance(text, str):
|
|
230
|
+
return set()
|
|
231
|
+
|
|
232
|
+
# Find all ${...} patterns
|
|
233
|
+
pattern = r'\$\{([^}]+)\}'
|
|
234
|
+
matches = re.findall(pattern, text)
|
|
235
|
+
|
|
236
|
+
# Extract simple parameter names (not complex paths like params.name or step[0].data)
|
|
237
|
+
simple_params = set()
|
|
238
|
+
for match in matches:
|
|
239
|
+
# Only consider simple parameter names (no dots, no brackets)
|
|
240
|
+
if '.' not in match and '[' not in match and ']' not in match:
|
|
241
|
+
simple_params.add(match)
|
|
242
|
+
|
|
243
|
+
return simple_params
|