662. Getting detailed information about structures
773. Generating structures with various options
884. Validating structure configurations
9+ 5. Managing named custom structure sources
910"""
1011import asyncio
1112import logging
1213import os
1314import sys
1415import yaml
16+ from types import SimpleNamespace
1517from typing import Any , Dict , Optional
1618
1719from fastmcp import FastMCP
1820
1921from structkit .commands .generate import GenerateCommand
2022from structkit .commands .validate import ValidateCommand
23+ from structkit .sources import (
24+ SourceConfigError ,
25+ add_source as add_config_source ,
26+ get_source_path ,
27+ read_sources ,
28+ remove_source as remove_config_source ,
29+ resolve_structures_path ,
30+ validate_source as validate_config_source ,
31+ )
2132from structkit import __version__
2233
2334
@@ -32,13 +43,20 @@ def __init__(self):
3243 # =====================
3344 # Tool logic (transport-agnostic)
3445 # =====================
35- def _list_structures_logic (self , structures_path : Optional [str ] = None ) -> str :
46+ def _list_structures_logic (self , structures_path : Optional [str ] = None , source : Optional [ str ] = None ) -> str :
3647 this_file = os .path .dirname (os .path .realpath (__file__ ))
3748 contribs_path = os .path .join (this_file , "contribs" )
3849
50+ try :
51+ effective_structures_path , _ = resolve_structures_path (
52+ SimpleNamespace (structures_path = structures_path , source = source )
53+ )
54+ except SourceConfigError as e :
55+ return f"❗ { e } "
56+
3957 paths_to_list = [contribs_path ]
40- if structures_path :
41- paths_to_list = [structures_path , contribs_path ]
58+ if effective_structures_path :
59+ paths_to_list = [effective_structures_path , contribs_path ]
4260
4361 all_structures = set ()
4462 for path in paths_to_list :
@@ -56,17 +74,28 @@ def _list_structures_logic(self, structures_path: Optional[str] = None) -> str:
5674 result_text += "\n \n Note: Structures with '+' sign are custom structures"
5775 return result_text
5876
59- def _get_structure_info_logic (self , structure_name : Optional [str ], structures_path : Optional [str ] = None ) -> str :
77+ def _get_structure_info_logic (
78+ self ,
79+ structure_name : Optional [str ],
80+ structures_path : Optional [str ] = None ,
81+ source : Optional [str ] = None ,
82+ ) -> str :
6083 if not structure_name :
6184 return "Error: structure_name is required"
6285
6386 # Resolve path
6487 if structure_name .startswith ("file://" ) and structure_name .endswith (".yaml" ):
6588 file_path = structure_name [7 :]
6689 else :
90+ try :
91+ effective_structures_path , resolved_structure_name = resolve_structures_path (
92+ SimpleNamespace (structures_path = structures_path , source = source ), structure_name
93+ )
94+ except SourceConfigError as e :
95+ return f"❗ { e } "
6796 this_file = os .path .dirname (os .path .realpath (__file__ ))
68- base = structures_path or os .path .join (this_file , "contribs" )
69- file_path = os .path .join (base , f"{ structure_name } .yaml" )
97+ base = effective_structures_path or os .path .join (this_file , "contribs" )
98+ file_path = os .path .join (base , f"{ resolved_structure_name } .yaml" )
7099
71100 if not os .path .exists (file_path ):
72101 return f"❗ Structure not found: { file_path } "
@@ -119,6 +148,7 @@ def _generate_structure_logic(
119148 dry_run : bool = False ,
120149 mappings : Optional [Dict [str , str ]] = None ,
121150 structures_path : Optional [str ] = None ,
151+ source : Optional [str ] = None ,
122152 ) -> str :
123153 class Args :
124154 pass
@@ -128,6 +158,7 @@ class Args:
128158 args .output = "console" if output == "console" else "file"
129159 args .dry_run = dry_run
130160 args .structures_path = structures_path
161+ args .source = source
131162 args .vars = None
132163 args .mappings_file = None
133164 args .backup = None
@@ -167,6 +198,54 @@ class Args:
167198 return f"Dry run completed for structure '{ structure_definition } ' at '{ base_path } '"
168199 return f"Structure '{ structure_definition } ' generated successfully at '{ base_path } '"
169200
201+ def _list_sources_logic (self ) -> str :
202+ sources = read_sources ()
203+ if not sources :
204+ return "No sources configured"
205+
206+ lines = ["📚 Configured structure sources\n " ]
207+ for name in sorted (sources ):
208+ lines .append (f" - { name } : { sources [name ]} \n " )
209+ return "" .join (lines ).rstrip ()
210+
211+ def _add_source_logic (self , name : Optional [str ], path_or_url : Optional [str ]) -> str :
212+ if not name :
213+ return "Error: name is required"
214+ if not path_or_url :
215+ return "Error: path_or_url is required"
216+ try :
217+ source_path = add_config_source (name , path_or_url )
218+ except SourceConfigError as e :
219+ return f"❗ { e } "
220+ return f"Added source '{ name } ': { source_path } "
221+
222+ def _remove_source_logic (self , name : Optional [str ]) -> str :
223+ if not name :
224+ return "Error: name is required"
225+ try :
226+ source_path = remove_config_source (name )
227+ except SourceConfigError as e :
228+ return f"❗ { e } "
229+ return f"Removed source '{ name } ': { source_path } "
230+
231+ def _show_source_logic (self , name : Optional [str ]) -> str :
232+ if not name :
233+ return "Error: name is required"
234+ try :
235+ source_path = get_source_path (name )
236+ except SourceConfigError as e :
237+ return f"❗ { e } "
238+ return f"{ name } : { source_path } "
239+
240+ def _validate_source_logic (self , name : Optional [str ]) -> str :
241+ if not name :
242+ return "Error: name is required"
243+ try :
244+ source_path = validate_config_source (name )
245+ except SourceConfigError as e :
246+ return f"❗ { e } "
247+ return f"Source '{ name } ' is valid: { source_path } "
248+
170249 def _validate_structure_logic (self , yaml_file : Optional [str ]) -> str :
171250 if not yaml_file :
172251 return "Error: yaml_file is required"
@@ -198,19 +277,23 @@ class Args:
198277 # =====================
199278 def _register_tools (self ):
200279 @self .app .tool (name = "list_structures" , description = "List all available structure definitions" )
201- async def list_structures (structures_path : Optional [str ] = None ) -> str :
202- self .logger .debug (f"MCP request: list_structures args={{'structures_path': { structures_path !r} }}" )
203- result = self ._list_structures_logic (structures_path )
280+ async def list_structures (structures_path : Optional [str ] = None , source : Optional [str ] = None ) -> str :
281+ self .logger .debug (
282+ f"MCP request: list_structures args={{'structures_path': { structures_path !r} , 'source': { source !r} }}"
283+ )
284+ result = self ._list_structures_logic (structures_path , source )
204285 preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
205286 self .logger .debug (f"MCP response: list_structures len={ len (result )} preview=\n { preview } " )
206287 return result
207288
208289 @self .app .tool (name = "get_structure_info" , description = "Get detailed information about a specific structure" )
209- async def get_structure_info (structure_name : str , structures_path : Optional [str ] = None ) -> str :
290+ async def get_structure_info (
291+ structure_name : str , structures_path : Optional [str ] = None , source : Optional [str ] = None
292+ ) -> str :
210293 self .logger .debug (
211- f"MCP request: get_structure_info args={{'structure_name': { structure_name !r} , 'structures_path': { structures_path !r} }}"
294+ f"MCP request: get_structure_info args={{'structure_name': { structure_name !r} , 'structures_path': { structures_path !r} , 'source': { source !r } }}"
212295 )
213- result = self ._get_structure_info_logic (structure_name , structures_path )
296+ result = self ._get_structure_info_logic (structure_name , structures_path , source )
214297 preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
215298 self .logger .debug (f"MCP response: get_structure_info len={ len (result )} preview=\n { preview } " )
216299 return result
@@ -223,6 +306,7 @@ async def generate_structure(
223306 dry_run : bool = False ,
224307 mappings : Optional [Dict [str , str ]] = None ,
225308 structures_path : Optional [str ] = None ,
309+ source : Optional [str ] = None ,
226310 ) -> str :
227311 self .logger .debug (
228312 "MCP request: generate_structure args=%s" ,
@@ -233,6 +317,7 @@ async def generate_structure(
233317 "dry_run" : dry_run ,
234318 "mappings" : mappings ,
235319 "structures_path" : structures_path ,
320+ "source" : source ,
236321 },
237322 )
238323 result = self ._generate_structure_logic (
@@ -242,11 +327,52 @@ async def generate_structure(
242327 dry_run ,
243328 mappings ,
244329 structures_path ,
330+ source ,
245331 )
246332 preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
247333 self .logger .debug (f"MCP response: generate_structure len={ len (result )} preview=\n { preview } " )
248334 return result
249335
336+ @self .app .tool (name = "list_sources" , description = "List configured custom structure sources" )
337+ async def list_sources () -> str :
338+ self .logger .debug ("MCP request: list_sources args={}" )
339+ result = self ._list_sources_logic ()
340+ preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
341+ self .logger .debug (f"MCP response: list_sources len={ len (result )} preview=\n { preview } " )
342+ return result
343+
344+ @self .app .tool (name = "add_source" , description = "Add or replace a local custom structure source" )
345+ async def add_source (name : str , path_or_url : str ) -> str :
346+ self .logger .debug (f"MCP request: add_source args={{'name': { name !r} , 'path_or_url': { path_or_url !r} }}" )
347+ result = self ._add_source_logic (name , path_or_url )
348+ preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
349+ self .logger .debug (f"MCP response: add_source len={ len (result )} preview=\n { preview } " )
350+ return result
351+
352+ @self .app .tool (name = "remove_source" , description = "Remove a configured custom structure source" )
353+ async def remove_source (name : str ) -> str :
354+ self .logger .debug (f"MCP request: remove_source args={{'name': { name !r} }}" )
355+ result = self ._remove_source_logic (name )
356+ preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
357+ self .logger .debug (f"MCP response: remove_source len={ len (result )} preview=\n { preview } " )
358+ return result
359+
360+ @self .app .tool (name = "show_source" , description = "Show a configured custom structure source" )
361+ async def show_source (name : str ) -> str :
362+ self .logger .debug (f"MCP request: show_source args={{'name': { name !r} }}" )
363+ result = self ._show_source_logic (name )
364+ preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
365+ self .logger .debug (f"MCP response: show_source len={ len (result )} preview=\n { preview } " )
366+ return result
367+
368+ @self .app .tool (name = "validate_source" , description = "Validate a configured custom structure source" )
369+ async def validate_source (name : str ) -> str :
370+ self .logger .debug (f"MCP request: validate_source args={{'name': { name !r} }}" )
371+ result = self ._validate_source_logic (name )
372+ preview = result if len (result ) <= 1000 else result [:1000 ] + f"... [truncated { len (result )- 1000 } chars]"
373+ self .logger .debug (f"MCP response: validate_source len={ len (result )} preview=\n { preview } " )
374+ return result
375+
250376 @self .app .tool (name = "validate_structure" , description = "Validate a structure configuration YAML file" )
251377 async def validate_structure (yaml_file : str ) -> str :
252378 self .logger .debug (f"MCP request: validate_structure args={{'yaml_file': { yaml_file !r} }}" )
@@ -323,8 +449,9 @@ async def _handle_get_structure_info(self, params: Dict[str, Any]):
323449 """Compatibility method for tests that expect MCP-style responses."""
324450 structure_name = params .get ('structure_name' )
325451 structures_path = params .get ('structures_path' )
452+ source = params .get ('source' )
326453
327- result_text = self ._get_structure_info_logic (structure_name , structures_path )
454+ result_text = self ._get_structure_info_logic (structure_name , structures_path , source )
328455
329456 # Mock MCP response structure
330457 class MockContent :
0 commit comments