Skip to content

Commit ac63e8f

Browse files
committed
wip
1 parent 2b24bd5 commit ac63e8f

3 files changed

Lines changed: 89 additions & 9 deletions

File tree

struct_module/commands/mcp.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def __init__(self, parser):
2323
help='Use stateless HTTP mode (HTTP transport only)')
2424
parser.add_argument('--no-banner', dest='show_banner', action='store_false', default=True,
2525
help='Disable FastMCP startup banner')
26+
# Debugging options
27+
parser.add_argument('--debug', action='store_true', help='Enable debug mode (sets struct and FastMCP loggers to DEBUG by default)')
28+
parser.add_argument('--fastmcp-log-level', dest='fastmcp_log_level', type=str, default=None,
29+
help='Log level for FastMCP internals (e.g., DEBUG, INFO). Overrides --debug for FastMCP if provided')
2630
parser.set_defaults(func=self.execute)
2731

2832
def execute(self, args):
@@ -41,15 +45,17 @@ def execute(self, args):
4145
print(" --path /PATH Endpoint path for HTTP/SSE (default: /mcp)")
4246
print(" --stateless-http Enable stateless HTTP mode (HTTP only)")
4347
print(" --no-banner Disable FastMCP banner")
48+
print(" --debug Enable debug mode (struct + FastMCP DEBUG; uvicorn=debug)")
49+
print(" --fastmcp-log-level LVL Set FastMCP logger level (overrides --debug for FastMCP)")
4450
print("\nMCP tools available:")
4551
print(" - list_structures: List all available structure definitions")
4652
print(" - get_structure_info: Get detailed information about a structure")
4753
print(" - generate_structure: Generate structures with various options")
4854
print(" - validate_structure: Validate structure configuration files")
4955
print("\nExamples:")
50-
print(" struct mcp --server --transport stdio")
51-
print(" struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp")
52-
print(" struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events")
56+
print(" struct mcp --server --transport stdio --debug")
57+
print(" struct mcp --server --transport http --host 127.0.0.1 --port 9000 --path /mcp --uvicorn-log-level debug")
58+
print(" struct mcp --server --transport sse --host 0.0.0.0 --port 8080 --path /events --fastmcp-log-level DEBUG")
5359

5460
async def _start_mcp_server(self, args=None):
5561
"""Start the MCP server using the selected transport."""
@@ -61,12 +67,30 @@ async def _start_mcp_server(self, args=None):
6167
"transport": transport,
6268
"show_banner": getattr(args, 'show_banner', True) if args else True,
6369
}
70+
# Determine FastMCP logger level
71+
fastmcp_log_level = None
72+
if args:
73+
fastmcp_log_level = getattr(args, 'fastmcp_log_level', None)
74+
if not fastmcp_log_level and getattr(args, 'debug', False):
75+
fastmcp_log_level = 'DEBUG'
76+
if fastmcp_log_level:
77+
run_kwargs["fastmcp_log_level"] = fastmcp_log_level
78+
6479
if transport in {"http", "sse"}:
80+
# uvicorn expects lowercase levels like "info"/"debug"
81+
uvicorn_level = None
82+
if args:
83+
uvicorn_level = getattr(args, 'uvicorn_log_level', None)
84+
if not uvicorn_level and getattr(args, 'debug', False):
85+
uvicorn_level = 'debug'
86+
if not uvicorn_level:
87+
# Default to args.log if provided, else None
88+
uvicorn_level = getattr(args, 'log', None)
6589
run_kwargs.update({
6690
"host": getattr(args, 'host', None),
6791
"port": getattr(args, 'port', None),
6892
"path": getattr(args, 'path', None),
69-
"log_level": getattr(args, 'uvicorn_log_level', None) or getattr(args, 'log', None),
93+
"log_level": (uvicorn_level.lower() if isinstance(uvicorn_level, str) else uvicorn_level),
7094
})
7195
if transport == "http":
7296
run_kwargs["stateless_http"] = getattr(args, 'stateless_http', None)

struct_module/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22
import logging
3+
import os
34
from dotenv import load_dotenv
45
from struct_module.utils import read_config_file, merge_configs
56
from struct_module.commands.generate import GenerateCommand
@@ -65,7 +66,16 @@ def main():
6566
file_config = read_config_file(args.config_file)
6667
args = argparse.Namespace(**merge_configs(file_config, args))
6768

68-
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
69+
# Resolve logging level precedence: STRUCT_LOG_LEVEL env > --debug (if present) > --log
70+
env_level = os.getenv('STRUCT_LOG_LEVEL')
71+
if env_level:
72+
logging_level = getattr(logging, env_level.upper(), logging.INFO)
73+
else:
74+
# Some commands (like mcp) may add a --debug flag; respect it
75+
if getattr(args, 'debug', False):
76+
logging_level = logging.DEBUG
77+
else:
78+
logging_level = getattr(logging, getattr(args, 'log', 'INFO').upper(), logging.INFO)
6979

7080
configure_logging(level=logging_level, log_file=getattr(args, 'log_file', None))
7181

struct_module/mcp_server.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,21 @@ class Args:
188188
def _register_tools(self):
189189
@self.app.tool(name="list_structures", description="List all available structure definitions")
190190
async def list_structures(structures_path: Optional[str] = None) -> str:
191-
return self._list_structures_logic(structures_path)
191+
self.logger.debug(f"MCP request: list_structures args={{'structures_path': {structures_path!r}}}")
192+
result = self._list_structures_logic(structures_path)
193+
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
194+
self.logger.debug(f"MCP response: list_structures len={len(result)} preview=\n{preview}")
195+
return result
192196

193197
@self.app.tool(name="get_structure_info", description="Get detailed information about a specific structure")
194198
async def get_structure_info(structure_name: str, structures_path: Optional[str] = None) -> str:
195-
return self._get_structure_info_logic(structure_name, structures_path)
199+
self.logger.debug(
200+
f"MCP request: get_structure_info args={{'structure_name': {structure_name!r}, 'structures_path': {structures_path!r}}}"
201+
)
202+
result = self._get_structure_info_logic(structure_name, structures_path)
203+
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
204+
self.logger.debug(f"MCP response: get_structure_info len={len(result)} preview=\n{preview}")
205+
return result
196206

197207
@self.app.tool(name="generate_structure", description="Generate a project structure using specified definition and options")
198208
async def generate_structure(
@@ -203,18 +213,36 @@ async def generate_structure(
203213
mappings: Optional[Dict[str, str]] = None,
204214
structures_path: Optional[str] = None,
205215
) -> str:
206-
return self._generate_structure_logic(
216+
self.logger.debug(
217+
"MCP request: generate_structure args=%s",
218+
{
219+
"structure_definition": structure_definition,
220+
"base_path": base_path,
221+
"output": output,
222+
"dry_run": dry_run,
223+
"mappings": mappings,
224+
"structures_path": structures_path,
225+
},
226+
)
227+
result = self._generate_structure_logic(
207228
structure_definition,
208229
base_path,
209230
output,
210231
dry_run,
211232
mappings,
212233
structures_path,
213234
)
235+
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
236+
self.logger.debug(f"MCP response: generate_structure len={len(result)} preview=\n{preview}")
237+
return result
214238

215239
@self.app.tool(name="validate_structure", description="Validate a structure configuration YAML file")
216240
async def validate_structure(yaml_file: str) -> str:
217-
return self._validate_structure_logic(yaml_file)
241+
self.logger.debug(f"MCP request: validate_structure args={{'yaml_file': {yaml_file!r}}}")
242+
result = self._validate_structure_logic(yaml_file)
243+
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
244+
self.logger.debug(f"MCP response: validate_structure len={len(result)} preview=\n{preview}")
245+
return result
218246

219247
async def run(
220248
self,
@@ -226,6 +254,7 @@ async def run(
226254
path: str | None = None,
227255
log_level: str | None = None,
228256
stateless_http: bool | None = None,
257+
fastmcp_log_level: str | None = None,
229258
):
230259
"""Run the FastMCP server with the specified transport.
231260
@@ -240,9 +269,16 @@ async def run(
240269
path: Endpoint path for HTTP/SSE transports
241270
log_level: Log level for the HTTP server (uvicorn)
242271
stateless_http: Whether to use stateless HTTP mode (HTTP only)
272+
fastmcp_log_level: Log level for FastMCP internals (e.g., DEBUG, INFO)
243273
"""
244274
loop = asyncio.get_running_loop()
245275
def _run():
276+
# Apply FastMCP-specific logger level if provided
277+
if fastmcp_log_level:
278+
try:
279+
logging.getLogger('fastmcp').setLevel(getattr(logging, fastmcp_log_level.upper()))
280+
except Exception:
281+
logging.getLogger('fastmcp').setLevel(logging.DEBUG if str(fastmcp_log_level).upper() == 'DEBUG' else logging.INFO)
246282
kwargs = {"show_banner": show_banner}
247283
if transport in {"http", "sse"}:
248284
if host is not None:
@@ -255,6 +291,16 @@ def _run():
255291
kwargs["log_level"] = log_level
256292
if stateless_http is not None and transport == "http":
257293
kwargs["stateless_http"] = stateless_http
294+
logging.getLogger(__name__).info(
295+
"Starting FastMCP %s server on http://%s:%s%s (uvicorn log_level=%s)",
296+
transport,
297+
kwargs.get("host", "127.0.0.1"),
298+
kwargs.get("port", 8000),
299+
kwargs.get("path", "/mcp"),
300+
kwargs.get("log_level", None),
301+
)
302+
else:
303+
logging.getLogger(__name__).info("Starting FastMCP stdio server")
258304
self.app.run(transport, **kwargs)
259305
await loop.run_in_executor(None, _run)
260306

0 commit comments

Comments
 (0)