solidworks-mcp-python 1.5.0__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.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Template Management tools for SolidWorks MCP Server.
|
|
2
|
+
|
|
3
|
+
Provides tools for managing SolidWorks templates including extraction, application,
|
|
4
|
+
comparison, and library management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from ..adapters.base import SolidWorksAdapter
|
|
13
|
+
from .file_facts import file_fact_snapshot
|
|
14
|
+
from .input_compat import CompatInput
|
|
15
|
+
from .tool_boundary import (
|
|
16
|
+
ToolOperationError,
|
|
17
|
+
UnsupportedOperationError,
|
|
18
|
+
require_capability,
|
|
19
|
+
require_success,
|
|
20
|
+
with_tool_boundary,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Input schemas for template management
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TemplateExtractionInput(BaseModel):
|
|
27
|
+
"""Input schema for extracting template from model.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
include_custom_properties (bool): The include custom properties value.
|
|
31
|
+
include_dimensions (bool): The include dimensions value.
|
|
32
|
+
save_path (str): The save path value.
|
|
33
|
+
source_model (str): The source model value.
|
|
34
|
+
template_name (str): The template name value.
|
|
35
|
+
template_type (str): The template type value.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
source_model: str = Field(description="Path to source model file")
|
|
39
|
+
template_name: str = Field(description="Name for the extracted template")
|
|
40
|
+
template_type: str = Field(description="Template type (part, assembly, drawing)")
|
|
41
|
+
save_path: str = Field(description="Path to save the template file")
|
|
42
|
+
include_custom_properties: bool = Field(
|
|
43
|
+
default=True, description="Include custom properties"
|
|
44
|
+
)
|
|
45
|
+
include_dimensions: bool = Field(default=True, description="Include dimensions")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TemplateApplicationInput(BaseModel):
|
|
49
|
+
"""Input schema for applying template to model.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
apply_dimensions (bool): The apply dimensions value.
|
|
53
|
+
apply_materials (bool): The apply materials value.
|
|
54
|
+
overwrite_existing (bool): The overwrite existing value.
|
|
55
|
+
target_model (str): The target model value.
|
|
56
|
+
template_path (str): The template path value.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
template_path: str = Field(description="Path to template file")
|
|
60
|
+
target_model: str = Field(description="Path to target model")
|
|
61
|
+
overwrite_existing: bool = Field(
|
|
62
|
+
default=False, description="Overwrite existing properties"
|
|
63
|
+
)
|
|
64
|
+
apply_dimensions: bool = Field(
|
|
65
|
+
default=True, description="Apply dimension formatting"
|
|
66
|
+
)
|
|
67
|
+
apply_materials: bool = Field(default=True, description="Apply material settings")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TemplateBatchInput(BaseModel):
|
|
71
|
+
"""Input schema for batch template operations.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
backup_originals (bool): The backup originals value.
|
|
75
|
+
file_pattern (str): The file pattern value.
|
|
76
|
+
recursive (bool): The recursive value.
|
|
77
|
+
source_folder (str): The source folder value.
|
|
78
|
+
template_path (str): The template path value.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
template_path: str = Field(description="Path to template file")
|
|
82
|
+
source_folder: str = Field(description="Folder containing target models")
|
|
83
|
+
file_pattern: str = Field(default="*.sldprt", description="File pattern to match")
|
|
84
|
+
recursive: bool = Field(default=True, description="Process subfolders")
|
|
85
|
+
backup_originals: bool = Field(default=True, description="Create backup copies")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TemplateComparisonInput(CompatInput):
|
|
89
|
+
"""Input schema for comparing templates.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
comparison_depth (str): The comparison depth value.
|
|
93
|
+
comparison_type (str): The comparison type value.
|
|
94
|
+
generate_report (bool): The generate report value.
|
|
95
|
+
include_dimensions (bool): The include dimensions value.
|
|
96
|
+
include_materials (bool): The include materials value.
|
|
97
|
+
include_properties (bool): The include properties value.
|
|
98
|
+
template1_path (str): The template1 path value.
|
|
99
|
+
template2_path (str): The template2 path value.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
template1_path: str = Field(description="Path to first template")
|
|
103
|
+
template2_path: str = Field(description="Path to second template")
|
|
104
|
+
comparison_type: str = Field(
|
|
105
|
+
default="full", description="Comparison type (full, properties, dimensions)"
|
|
106
|
+
)
|
|
107
|
+
comparison_depth: str = Field(default="full", description="Comparison depth alias")
|
|
108
|
+
include_properties: bool = Field(default=True, description="Include properties")
|
|
109
|
+
include_dimensions: bool = Field(default=True, description="Include dimensions")
|
|
110
|
+
include_materials: bool = Field(default=True, description="Include materials")
|
|
111
|
+
generate_report: bool = Field(
|
|
112
|
+
default=True, description="Generate comparison report"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def register_template_management_tools(
|
|
117
|
+
mcp: FastMCP, adapter: SolidWorksAdapter, config: Any
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Register template management tools with FastMCP.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
mcp (FastMCP): The mcp value.
|
|
123
|
+
adapter (SolidWorksAdapter): Adapter instance used for the operation.
|
|
124
|
+
config (Any): Configuration values for the operation.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
int: The computed numeric result.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> tool_count = await register_template_management_tools(mcp, adapter, config)
|
|
131
|
+
"""
|
|
132
|
+
mcp = with_tool_boundary(mcp)
|
|
133
|
+
tool_count = 0
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
async def extract_template(input_data: TemplateExtractionInput) -> dict[str, Any]:
|
|
137
|
+
"""Extract template from existing SolidWorks model.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
input_data (TemplateExtractionInput): The input data value.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> result = await extract_template(extraction_input)
|
|
147
|
+
"""
|
|
148
|
+
require_capability(
|
|
149
|
+
adapter,
|
|
150
|
+
"extract_template",
|
|
151
|
+
"Active adapter does not support extract_template; no "
|
|
152
|
+
f"template was extracted from {input_data.source_model}.",
|
|
153
|
+
)
|
|
154
|
+
result = await adapter.extract_template(input_data.model_dump())
|
|
155
|
+
require_success(result, "Failed to extract template")
|
|
156
|
+
return {
|
|
157
|
+
"status": "success",
|
|
158
|
+
"message": f"Template '{input_data.template_name}' extracted from {input_data.source_model}",
|
|
159
|
+
"data": result.data,
|
|
160
|
+
"execution_time": result.execution_time,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
async def apply_template(input_data: TemplateApplicationInput) -> dict[str, Any]:
|
|
165
|
+
"""Apply a template to an existing SolidWorks model.
|
|
166
|
+
|
|
167
|
+
This tool applies saved template settings including properties, dimensions, and
|
|
168
|
+
formatting to the target model.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
input_data (TemplateApplicationInput): The input data value.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> result = await apply_template(application_input)
|
|
178
|
+
"""
|
|
179
|
+
require_capability(
|
|
180
|
+
adapter,
|
|
181
|
+
"apply_template",
|
|
182
|
+
"Active adapter does not support apply_template; "
|
|
183
|
+
f"{input_data.target_model} was not changed.",
|
|
184
|
+
)
|
|
185
|
+
result = await adapter.apply_template(input_data.model_dump())
|
|
186
|
+
require_success(result, "Failed to apply template")
|
|
187
|
+
return {
|
|
188
|
+
"status": "success",
|
|
189
|
+
"message": f"Template applied to {input_data.target_model}",
|
|
190
|
+
"data": result.data,
|
|
191
|
+
"execution_time": result.execution_time,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@mcp.tool()
|
|
195
|
+
async def batch_apply_template(input_data: TemplateBatchInput) -> dict[str, Any]:
|
|
196
|
+
"""Apply template to multiple models in batch.
|
|
197
|
+
|
|
198
|
+
This tool processes multiple SolidWorks files and applies the same template
|
|
199
|
+
configuration to all matching files.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
input_data (TemplateBatchInput): The input data value.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> result = await batch_apply_template(batch_input)
|
|
209
|
+
"""
|
|
210
|
+
require_capability(
|
|
211
|
+
adapter,
|
|
212
|
+
"batch_apply_template",
|
|
213
|
+
"Active adapter does not support batch_apply_template; "
|
|
214
|
+
f"no files in {input_data.source_folder} were changed.",
|
|
215
|
+
)
|
|
216
|
+
result = await adapter.batch_apply_template(input_data.model_dump())
|
|
217
|
+
require_success(result, "Failed batch template application")
|
|
218
|
+
return {
|
|
219
|
+
"status": "success",
|
|
220
|
+
"message": "Batch template application completed",
|
|
221
|
+
"data": result.data,
|
|
222
|
+
"execution_time": result.execution_time,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def compare_templates(input_data: TemplateComparisonInput) -> dict[str, Any]:
|
|
227
|
+
"""Compare two templates and generate difference report.
|
|
228
|
+
|
|
229
|
+
This tool analyzes differences between templates to help understand variations in
|
|
230
|
+
formatting and properties.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
input_data (TemplateComparisonInput): The input data value.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
>>> result = await compare_templates(comparison_input)
|
|
240
|
+
|
|
241
|
+
Note:
|
|
242
|
+
When no adapter can parse template internals, this falls back to
|
|
243
|
+
a filesystem-level comparison only (existence, size, modified
|
|
244
|
+
time, byte-identical check) — it does not report property,
|
|
245
|
+
dimension, or formatting differences it cannot actually read.
|
|
246
|
+
"""
|
|
247
|
+
if hasattr(adapter, "compare_templates"):
|
|
248
|
+
result = await adapter.compare_templates(input_data.model_dump())
|
|
249
|
+
require_success(result, "Failed to compare templates")
|
|
250
|
+
return {
|
|
251
|
+
"status": "success",
|
|
252
|
+
"message": "Template comparison completed",
|
|
253
|
+
"data": result.data,
|
|
254
|
+
"execution_time": result.execution_time,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
file_1 = file_fact_snapshot(input_data.template1_path)
|
|
258
|
+
file_2 = file_fact_snapshot(input_data.template2_path)
|
|
259
|
+
|
|
260
|
+
missing = [
|
|
261
|
+
path
|
|
262
|
+
for path, snapshot in (
|
|
263
|
+
(input_data.template1_path, file_1),
|
|
264
|
+
(input_data.template2_path, file_2),
|
|
265
|
+
)
|
|
266
|
+
if snapshot is None
|
|
267
|
+
]
|
|
268
|
+
if missing:
|
|
269
|
+
raise ToolOperationError(
|
|
270
|
+
"Cannot compare templates; file(s) not found on "
|
|
271
|
+
f"disk: {', '.join(missing)}. No template-property "
|
|
272
|
+
"diff was performed."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
assert file_1 is not None
|
|
276
|
+
assert file_2 is not None
|
|
277
|
+
identical = file_1["sha256"] == file_2["sha256"]
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"status": "success",
|
|
281
|
+
"message": (
|
|
282
|
+
"Template files are byte-identical"
|
|
283
|
+
if identical
|
|
284
|
+
else "Template files differ on disk"
|
|
285
|
+
),
|
|
286
|
+
"comparison": {
|
|
287
|
+
"file_1": file_1,
|
|
288
|
+
"file_2": file_2,
|
|
289
|
+
"identical": identical,
|
|
290
|
+
"size_delta_bytes": file_2["size_bytes"] - file_1["size_bytes"],
|
|
291
|
+
},
|
|
292
|
+
"note": (
|
|
293
|
+
"This is a filesystem-level comparison only; template "
|
|
294
|
+
"properties, dimensions, and formatting were not "
|
|
295
|
+
"analyzed."
|
|
296
|
+
),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@mcp.tool()
|
|
300
|
+
async def save_to_template_library(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
301
|
+
"""Save template to the organization's template library.
|
|
302
|
+
|
|
303
|
+
This tool manages a centralized template library with categorization and version
|
|
304
|
+
control.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
input_data (dict[str, Any]): The input data value.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
>>> result = await save_to_template_library(library_input)
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
if hasattr(adapter, "save_to_template_library"):
|
|
317
|
+
result = await adapter.save_to_template_library(input_data)
|
|
318
|
+
require_success(result, "Failed to save to library")
|
|
319
|
+
return {
|
|
320
|
+
"status": "success",
|
|
321
|
+
"message": "Template saved to library",
|
|
322
|
+
"data": result.data,
|
|
323
|
+
"execution_time": result.execution_time,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
template_name = input_data.get("template_name", "")
|
|
327
|
+
|
|
328
|
+
raise UnsupportedOperationError(
|
|
329
|
+
"Active adapter does not support save_to_template_library; "
|
|
330
|
+
f"'{template_name}' was not saved to any library. There is "
|
|
331
|
+
"no local template registry — this tool needs a real "
|
|
332
|
+
"adapter-backed library service."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@mcp.tool()
|
|
336
|
+
async def list_template_library(input_data: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
"""List available templates from the template library.
|
|
338
|
+
|
|
339
|
+
This tool provides browsing and searching capabilities for the organization's template
|
|
340
|
+
library.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
input_data (dict[str, Any]): The input data value.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> result = await list_template_library(list_input)
|
|
350
|
+
"""
|
|
351
|
+
require_capability(
|
|
352
|
+
adapter,
|
|
353
|
+
"list_template_library",
|
|
354
|
+
"Active adapter does not support list_template_library; "
|
|
355
|
+
"there is no local template registry to list.",
|
|
356
|
+
)
|
|
357
|
+
result = await adapter.list_template_library(input_data)
|
|
358
|
+
require_success(result, "Failed to list template library")
|
|
359
|
+
return {
|
|
360
|
+
"status": "success",
|
|
361
|
+
"message": "Template library listed successfully",
|
|
362
|
+
"data": result.data,
|
|
363
|
+
"execution_time": result.execution_time,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
tool_count = 6 # Template management tools
|
|
367
|
+
return tool_count
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""The single error boundary every MCP tool runs inside (spec: tools/error-boundary).
|
|
2
|
+
|
|
3
|
+
Tools contain business logic only. They:
|
|
4
|
+
|
|
5
|
+
* parse input with :func:`normalize_input` (raises :class:`InvalidInputError`),
|
|
6
|
+
* check adapter results with :func:`require_success` (raises
|
|
7
|
+
:class:`ToolOperationError`), and
|
|
8
|
+
* return their success payload.
|
|
9
|
+
|
|
10
|
+
:func:`tool_boundary` turns anything raised into the uniform error payload -
|
|
11
|
+
``status``, ``message``, ``error_type``, ``hint`` and ``execution_time`` (the
|
|
12
|
+
adapter's timing, else the tool's own wall time) - using :class:`ErrorClassifier` to name the failure.
|
|
13
|
+
:class:`ToolRegistrar` applies the boundary to every tool at registration, so no
|
|
14
|
+
tool can forget it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import functools
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Awaitable, Callable
|
|
22
|
+
from typing import Any, cast
|
|
23
|
+
|
|
24
|
+
from loguru import logger
|
|
25
|
+
from pydantic import BaseModel, ValidationError
|
|
26
|
+
|
|
27
|
+
from ..adapters.base import AdapterResult
|
|
28
|
+
from .error_classifier import (
|
|
29
|
+
INTERNAL,
|
|
30
|
+
INVALID_INPUT,
|
|
31
|
+
NOT_SUPPORTED,
|
|
32
|
+
NOT_SUPPORTED_HINT,
|
|
33
|
+
Classification,
|
|
34
|
+
ErrorClassifier,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_INVALID_INPUT_HINT = (
|
|
38
|
+
"Fix the named field(s) and call the tool again; see the tool's input "
|
|
39
|
+
"schema for the expected types and units."
|
|
40
|
+
)
|
|
41
|
+
_CLASSIFIER = ErrorClassifier()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InvalidInputError(ValueError):
|
|
45
|
+
"""The tool's input failed validation; the adapter was not called."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ToolOperationError(Exception):
|
|
49
|
+
"""An adapter operation reported failure.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
execution_time: The adapter's timing for the failed call, if any.
|
|
53
|
+
data: Diagnostic data that explains the failure (e.g. rebuild errors),
|
|
54
|
+
returned to the caller under ``data``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str,
|
|
60
|
+
execution_time: float | None = None,
|
|
61
|
+
data: Any = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(message)
|
|
64
|
+
self.execution_time = execution_time
|
|
65
|
+
self.data = data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UnsupportedOperationError(Exception):
|
|
69
|
+
"""The active adapter has no capability for the requested operation."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def require_capability(adapter: Any, method: str, message: str) -> None:
|
|
73
|
+
"""Raise :class:`UnsupportedOperationError` unless ``adapter`` has ``method``.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
adapter: The active SolidWorks adapter.
|
|
77
|
+
method: The adapter method the tool needs.
|
|
78
|
+
message: What was not done, and what to do instead.
|
|
79
|
+
"""
|
|
80
|
+
if not hasattr(adapter, method):
|
|
81
|
+
raise UnsupportedOperationError(message)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def normalize_input[TModel: BaseModel](
|
|
85
|
+
input_data: Any, model_type: type[TModel]
|
|
86
|
+
) -> TModel:
|
|
87
|
+
"""Validate ``input_data`` as ``model_type``.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
input_data: A ``model_type`` instance, a mapping of its fields, another
|
|
91
|
+
pydantic model (converted through ``model_dump``), or ``None`` for
|
|
92
|
+
a tool whose fields all have defaults.
|
|
93
|
+
model_type: The tool's pydantic input model.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
TModel: The validated model.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
InvalidInputError: Naming the offending field(s).
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(input_data, model_type):
|
|
102
|
+
return input_data
|
|
103
|
+
if input_data is None:
|
|
104
|
+
input_data = {}
|
|
105
|
+
elif not isinstance(input_data, dict) and hasattr(input_data, "model_dump"):
|
|
106
|
+
input_data = input_data.model_dump()
|
|
107
|
+
try:
|
|
108
|
+
return model_type.model_validate(input_data)
|
|
109
|
+
except ValidationError as exc: # also wraps ValueErrors from validators
|
|
110
|
+
raise InvalidInputError(describe_validation_error(exc)) from exc
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def describe_validation_error(exc: ValidationError) -> str:
|
|
114
|
+
"""Render a pydantic error as ``field: problem; field: problem``."""
|
|
115
|
+
parts = []
|
|
116
|
+
for error in exc.errors():
|
|
117
|
+
field = ".".join(str(p) for p in error.get("loc", ())) or "input"
|
|
118
|
+
parts.append(f"{field}: {error.get('msg', 'invalid')}")
|
|
119
|
+
return "; ".join(parts)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def require_success[TResult](
|
|
123
|
+
result: AdapterResult[TResult],
|
|
124
|
+
failure_message: str = "Operation failed",
|
|
125
|
+
*,
|
|
126
|
+
context: str | None = None,
|
|
127
|
+
with_data: bool = False,
|
|
128
|
+
) -> AdapterResult[TResult]:
|
|
129
|
+
"""Return ``result`` when it succeeded; raise otherwise.
|
|
130
|
+
|
|
131
|
+
Mirrors the ``if result.is_success: ... else: error`` the tools used before
|
|
132
|
+
the boundary, so anything that is not a success - including a warning - is
|
|
133
|
+
reported as a failure. Tools that treat a warning as success check
|
|
134
|
+
``result.status`` themselves before calling this.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
result: The adapter result to check.
|
|
138
|
+
failure_message: Message used when the adapter gave none.
|
|
139
|
+
context: Prefix naming what failed, e.g. ``"Failed to create part"``,
|
|
140
|
+
giving ``"<context>: <adapter error>"``.
|
|
141
|
+
with_data: Return the adapter's ``data`` with the error, for tools whose
|
|
142
|
+
failure data is the diagnosis (e.g. ``rebuild_model``).
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ToolOperationError: Carrying the adapter's message and timing.
|
|
146
|
+
"""
|
|
147
|
+
if not result.is_success:
|
|
148
|
+
message = str(result.error or failure_message)
|
|
149
|
+
raise ToolOperationError(
|
|
150
|
+
f"{context}: {message}" if context else message,
|
|
151
|
+
result.execution_time,
|
|
152
|
+
result.data if with_data else None,
|
|
153
|
+
)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def error_payload(
|
|
158
|
+
message: str,
|
|
159
|
+
classification: Classification,
|
|
160
|
+
execution_time: float | None = None,
|
|
161
|
+
data: Any = None,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Build the spec's error payload."""
|
|
164
|
+
payload: dict[str, Any] = {
|
|
165
|
+
"status": "error",
|
|
166
|
+
"message": message,
|
|
167
|
+
"error_type": classification.error_type,
|
|
168
|
+
"hint": classification.hint,
|
|
169
|
+
}
|
|
170
|
+
payload["execution_time"] = execution_time
|
|
171
|
+
if data is not None:
|
|
172
|
+
payload["data"] = data
|
|
173
|
+
return payload
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def tool_boundary(
|
|
177
|
+
fn: Callable[..., Awaitable[dict[str, Any]]],
|
|
178
|
+
classifier: ErrorClassifier = _CLASSIFIER,
|
|
179
|
+
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
180
|
+
"""Wrap an async tool so every failure becomes an error payload.
|
|
181
|
+
|
|
182
|
+
``functools.wraps`` keeps the signature, so FastMCP builds the same input
|
|
183
|
+
schema for the wrapped tool.
|
|
184
|
+
"""
|
|
185
|
+
tool_name = fn.__name__
|
|
186
|
+
|
|
187
|
+
@functools.wraps(fn)
|
|
188
|
+
async def wrapper(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
189
|
+
started = time.perf_counter()
|
|
190
|
+
|
|
191
|
+
def elapsed() -> float:
|
|
192
|
+
return time.perf_counter() - started
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
return await fn(*args, **kwargs)
|
|
196
|
+
except InvalidInputError as exc:
|
|
197
|
+
return error_payload(
|
|
198
|
+
f"Invalid input: {exc}",
|
|
199
|
+
Classification(INVALID_INPUT, _INVALID_INPUT_HINT),
|
|
200
|
+
elapsed(),
|
|
201
|
+
)
|
|
202
|
+
except ValidationError as exc:
|
|
203
|
+
return error_payload(
|
|
204
|
+
f"Invalid input: {describe_validation_error(exc)}",
|
|
205
|
+
Classification(INVALID_INPUT, _INVALID_INPUT_HINT),
|
|
206
|
+
elapsed(),
|
|
207
|
+
)
|
|
208
|
+
except UnsupportedOperationError as exc:
|
|
209
|
+
return error_payload(
|
|
210
|
+
str(exc),
|
|
211
|
+
Classification(NOT_SUPPORTED, NOT_SUPPORTED_HINT),
|
|
212
|
+
elapsed(),
|
|
213
|
+
)
|
|
214
|
+
except ToolOperationError as exc:
|
|
215
|
+
message = str(exc)
|
|
216
|
+
# Adapter guards (wrong document type, no active model) reject a
|
|
217
|
+
# call before timing it; fall back to the tool's own wall time.
|
|
218
|
+
timing = exc.execution_time if exc.execution_time is not None else elapsed()
|
|
219
|
+
return error_payload(
|
|
220
|
+
message, classifier.classify(message), timing, exc.data
|
|
221
|
+
)
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
logger.exception("Tool {} raised", tool_name)
|
|
224
|
+
message = f"{tool_name} failed: {type(exc).__name__}: {exc}"
|
|
225
|
+
return error_payload(
|
|
226
|
+
message,
|
|
227
|
+
classifier.classify(str(exc), exc, fallback=INTERNAL),
|
|
228
|
+
elapsed(),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
wrapper.__tool_boundary__ = True # type: ignore[attr-defined]
|
|
232
|
+
return wrapper
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def has_tool_boundary(fn: Any) -> bool:
|
|
236
|
+
"""True when ``fn`` was wrapped by :func:`tool_boundary`."""
|
|
237
|
+
return bool(getattr(fn, "__tool_boundary__", False))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ToolRegistrar:
|
|
241
|
+
"""A FastMCP stand-in whose ``tool()`` applies :func:`tool_boundary` first.
|
|
242
|
+
|
|
243
|
+
Everything else is forwarded to the wrapped server unchanged.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self, mcp: Any, classifier: ErrorClassifier = _CLASSIFIER) -> None:
|
|
247
|
+
self._mcp = mcp
|
|
248
|
+
self._classifier = classifier
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def of(cls, mcp: Any) -> ToolRegistrar:
|
|
252
|
+
"""Wrap ``mcp`` unless it already is a registrar (idempotent).
|
|
253
|
+
|
|
254
|
+
Every ``register_<area>_tools`` starts with ``mcp = ToolRegistrar.of(mcp)``
|
|
255
|
+
so its tools get the boundary however the module is registered.
|
|
256
|
+
"""
|
|
257
|
+
return mcp if isinstance(mcp, cls) else cls(mcp)
|
|
258
|
+
|
|
259
|
+
def tool(self, *args: Any, **kwargs: Any) -> Any:
|
|
260
|
+
"""Same contract as ``FastMCP.tool``, with the boundary applied."""
|
|
261
|
+
if args and callable(args[0]) and len(args) == 1 and not kwargs:
|
|
262
|
+
return self._mcp.tool()(tool_boundary(args[0], self._classifier))
|
|
263
|
+
register = self._mcp.tool(*args, **kwargs)
|
|
264
|
+
|
|
265
|
+
def decorator(fn: Callable[..., Awaitable[dict[str, Any]]]) -> Any:
|
|
266
|
+
return register(tool_boundary(fn, self._classifier))
|
|
267
|
+
|
|
268
|
+
return decorator
|
|
269
|
+
|
|
270
|
+
def __getattr__(self, name: str) -> Any:
|
|
271
|
+
return getattr(self._mcp, name)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def with_tool_boundary[TServer](mcp: TServer) -> TServer:
|
|
275
|
+
"""Return ``mcp`` wrapped in a :class:`ToolRegistrar` (typed as ``mcp``).
|
|
276
|
+
|
|
277
|
+
The registrar duck-types the FastMCP surface the tool modules use
|
|
278
|
+
(``tool()`` plus attribute forwarding), so modules keep their annotations.
|
|
279
|
+
"""
|
|
280
|
+
return cast(TServer, ToolRegistrar.of(mcp))
|