|
6 | 6 | 2. Getting detailed information about structures |
7 | 7 | 3. Generating structures with various options |
8 | 8 | 4. Validating structure configurations |
| 9 | +5. Explaining structure resolution without side effects |
9 | 10 | """ |
10 | 11 | import asyncio |
11 | 12 | import logging |
|
17 | 18 | from fastmcp import FastMCP |
18 | 19 |
|
19 | 20 | from structkit.commands.generate import GenerateCommand |
| 21 | +from structkit.commands.explain import ExplainCommand |
20 | 22 | from structkit.commands.validate import ValidateCommand |
21 | 23 | from structkit import __version__ |
22 | 24 |
|
@@ -193,6 +195,56 @@ class Args: |
193 | 195 | finally: |
194 | 196 | sys.stdout = old |
195 | 197 |
|
| 198 | + def _explain_structure_logic( |
| 199 | + self, |
| 200 | + structure_definition: str, |
| 201 | + base_path: str = ".", |
| 202 | + output: str = "text", |
| 203 | + variables: Optional[Dict[str, str]] = None, |
| 204 | + mappings: Optional[Dict[str, Any]] = None, |
| 205 | + structures_path: Optional[str] = None, |
| 206 | + file_strategy: str = "overwrite", |
| 207 | + ) -> str: |
| 208 | + if not structure_definition: |
| 209 | + return "Error: structure_definition is required" |
| 210 | + |
| 211 | + valid_outputs = {"text", "json"} |
| 212 | + if output not in valid_outputs: |
| 213 | + return f"Error: output must be one of {sorted(valid_outputs)}, got: {output}" |
| 214 | + |
| 215 | + valid_file_strategies = {"overwrite", "skip", "append", "rename", "backup"} |
| 216 | + if file_strategy not in valid_file_strategies: |
| 217 | + return f"Error: file_strategy must be one of {sorted(valid_file_strategies)}, got: {file_strategy}" |
| 218 | + |
| 219 | + class Args: |
| 220 | + pass |
| 221 | + args = Args() |
| 222 | + args.structure_definition = structure_definition |
| 223 | + args.base_path = base_path or "." |
| 224 | + args.output = output |
| 225 | + args.json_output = output == "json" |
| 226 | + args.structures_path = structures_path |
| 227 | + args.vars = None |
| 228 | + args.mappings_file = None |
| 229 | + args.file_strategy = file_strategy |
| 230 | + args.log = "INFO" |
| 231 | + args.config_file = None |
| 232 | + args.log_file = None |
| 233 | + |
| 234 | + if variables: |
| 235 | + args.vars = ",".join([f"{k}={v}" for k, v in variables.items()]) |
| 236 | + |
| 237 | + import argparse |
| 238 | + dummy_parser = argparse.ArgumentParser() |
| 239 | + command = ExplainCommand(dummy_parser) |
| 240 | + explanation = command.explain(args, mappings=mappings or {}) |
| 241 | + if explanation is None: |
| 242 | + return f"Unable to explain structure '{structure_definition}'" |
| 243 | + if output == "json": |
| 244 | + import json |
| 245 | + return json.dumps(explanation, indent=2, sort_keys=True) |
| 246 | + return command._format_text(explanation) |
| 247 | + |
196 | 248 | # ===================== |
197 | 249 | # FastMCP tool registration (maps to logic above) |
198 | 250 | # ===================== |
@@ -255,6 +307,41 @@ async def validate_structure(yaml_file: str) -> str: |
255 | 307 | self.logger.debug(f"MCP response: validate_structure len={len(result)} preview=\n{preview}") |
256 | 308 | return result |
257 | 309 |
|
| 310 | + @self.app.tool(name="explain_structure", description="Explain structure resolution without creating files, fetching remote content, or running hooks") |
| 311 | + async def explain_structure( |
| 312 | + structure_definition: str, |
| 313 | + base_path: str = ".", |
| 314 | + output: str = "text", |
| 315 | + variables: Optional[Dict[str, str]] = None, |
| 316 | + mappings: Optional[Dict[str, Any]] = None, |
| 317 | + structures_path: Optional[str] = None, |
| 318 | + file_strategy: str = "overwrite", |
| 319 | + ) -> str: |
| 320 | + self.logger.debug( |
| 321 | + "MCP request: explain_structure args=%s", |
| 322 | + { |
| 323 | + "structure_definition": structure_definition, |
| 324 | + "base_path": base_path, |
| 325 | + "output": output, |
| 326 | + "variables": variables, |
| 327 | + "mappings": mappings, |
| 328 | + "structures_path": structures_path, |
| 329 | + "file_strategy": file_strategy, |
| 330 | + }, |
| 331 | + ) |
| 332 | + result = self._explain_structure_logic( |
| 333 | + structure_definition, |
| 334 | + base_path, |
| 335 | + output, |
| 336 | + variables, |
| 337 | + mappings, |
| 338 | + structures_path, |
| 339 | + file_strategy, |
| 340 | + ) |
| 341 | + preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]" |
| 342 | + self.logger.debug(f"MCP response: explain_structure len={len(result)} preview=\n{preview}") |
| 343 | + return result |
| 344 | + |
258 | 345 | async def run( |
259 | 346 | self, |
260 | 347 | transport: str = "stdio", |
|
0 commit comments