Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The `struct` CLI allows you to generate project structures from YAML configurati
**Basic Usage:**

```sh
structkit {info,validate,generate,list,generate-schema,mcp,completion,init} ...
structkit {info,validate,generate,vars,list,generate-schema,mcp,completion,init} ...
```

## Global Options
Expand Down Expand Up @@ -115,6 +115,30 @@ structkit generate
- `--mappings-file MAPPINGS_FILE`: Path to a YAML file containing mappings to be used in templates (can be specified multiple times).
- `-o {console,file}, --output {console,file}`: Output mode.

### `vars`

Inspect variables declared by a structure definition without generating files.

**Usage:**

```sh
structkit vars [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTURES_PATH] [--json] structure_definition
```

**Arguments:**

- `structure_definition`: Built-in structure name, custom structure name, or local YAML file path. Local `.yaml` and `.yml` files can be passed directly, or with `file://`.
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to custom structure definitions. Can be set via the `STRUCTKIT_STRUCTURES_PATH` environment variable.
- `--json`: Print machine-readable JSON with each variable's name, type, default value, description/help text, and required status.

Examples:

```sh
structkit vars project/python
structkit vars ./my-struct.yaml --json
structkit vars python-basic --structures-path ~/custom-structures
```

### `list`

List available structures.
Expand Down
22 changes: 21 additions & 1 deletion docs/mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,26 @@ Generate a project structure using specified definition and options.
- `mappings` (optional): Variable mappings for template substitution
- `structures_path` (optional): Custom path to structure definitions

### 4. validate_structure
### 4. get_structure_vars
Inspect variables declared by a specific structure without generating files.

```json
{
"name": "get_structure_vars",
"arguments": {
"structure_name": "project/python",
"structures_path": "/path/to/custom/structures", // optional
"output": "json" // "text" or "json", optional
}
}
```

**Parameters:**
- `structure_name` (required): Name or local YAML path of the structure to inspect
- `structures_path` (optional): Custom path to structure definitions
- `output` (optional): Output format - "text" for aligned human-readable output or "json" for machine-readable output (default: "text")

### 5. validate_structure
Validate a structure configuration YAML file.

```json
Expand Down Expand Up @@ -353,6 +372,7 @@ Once connected, you can use these tools:
- `list_structures` - Get all available structures
- `get_structure_info` - Get details about a specific structure
- `generate_structure` - Generate project structures
- `get_structure_vars` - Inspect declared structure variables
- `validate_structure` - Validate YAML configuration files

## Troubleshooting
Expand Down
14 changes: 14 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Run the script with the following command using one of the following subcommands
- `generate-schema`: Generate JSON schema for available structure templates.
- `validate`: Validate the YAML configuration file.
- `info`: Display information about the script and its dependencies.
- `vars`: Inspect variables declared by a structure definition without generating files.
- `list`: List the available structs

For more information, run the script with the `-h` or `--help` option (this is also available for each subcommand):
Expand Down Expand Up @@ -145,6 +146,19 @@ The file includes:
- A README.md placeholder in files
- A folders entry pointing to the github/workflows/run-structkit workflow at ./


### Inspect Variables

Use `structkit vars` to see the inputs a structure declares before running `generate`. The command supports built-in structures, custom structures via `--structures-path`, and local YAML files without creating any files.

```sh
structkit vars project/python
structkit vars ./my-struct.yaml --json
structkit vars python-basic --structures-path ~/custom-structures
```

Text output lists each variable's name, type, default value, description/help text, and whether it is required or optional. Use `--json` for CI and other machine-readable workflows.

### Validate Configuration

```sh
Expand Down
133 changes: 133 additions & 0 deletions structkit/commands/vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import json
import os
import yaml

from structkit.commands import Command
from structkit.completers import structures_completer


class VarsCommand(Command):
"""Inspect variables declared by a structure definition."""

def __init__(self, parser):
super().__init__(parser)
parser.description = "Inspect variables declared by a structure definition"
structure_arg = parser.add_argument('structure_definition', type=str, help='Structure definition name or path to a YAML file')
structure_arg.completer = structures_completer
parser.add_argument(
'-s',
'--structures-path',
type=str,
help='Path to structure definitions (env: STRUCTKIT_STRUCTURES_PATH)',
default=os.getenv('STRUCTKIT_STRUCTURES_PATH', None)
)
parser.add_argument('--json', action='store_true', help='Output variables as JSON')
parser.set_defaults(func=self.execute)

def execute(self, args):
config = self._load_yaml_config(args.structure_definition, args.structures_path)
if config is None:
raise SystemExit(1)
if not isinstance(config, dict):
self.logger.error("❗ Invalid structure config: top-level YAML content must be a mapping")
raise SystemExit(1)

try:
variables = self._normalize_variables(config.get('variables', []))
except ValueError as exc:
self.logger.error(f"❗ Invalid variables config: {exc}")
raise SystemExit(1) from exc

if args.json:
print(json.dumps(variables, indent=2))
else:
self._print_text(args.structure_definition, variables)

def _load_yaml_config(self, structure_definition, structures_path):
if structure_definition.endswith(('.yaml', '.yml')) and not structure_definition.startswith("file://"):
structure_definition = f"file://{structure_definition}"

if structure_definition.startswith("file://") and structure_definition.endswith((".yaml", ".yml")):
file_path = structure_definition[7:]
else:
this_file = os.path.dirname(os.path.realpath(__file__))
contribs_path = os.path.join(this_file, "..", "contribs")
file_path = os.path.join(contribs_path, f"{structure_definition}.yaml")
if structures_path:
file_path = os.path.join(structures_path, f"{structure_definition}.yaml")
if not os.path.exists(file_path):
file_path = os.path.join(contribs_path, f"{structure_definition}.yaml")

if not os.path.exists(file_path):
self.logger.error(f"❗ File not found: {file_path}")
return None

try:
with open(file_path, 'r') as f:
return yaml.safe_load(f) or {}
except yaml.YAMLError as exc:
self.logger.error(f"❗ Invalid YAML in {file_path}: {exc}")
return None
except OSError as exc:
self.logger.error(f"❗ Failed to read {file_path}: {exc}")
return None

def _normalize_variables(self, variables):
if variables is None:
return []
if not isinstance(variables, list):
raise ValueError("the 'variables' key must be a list")

normalized = []
for item in variables:
if not isinstance(item, dict):
raise ValueError("each variable entry must be a mapping")
for name, content in item.items():
if not isinstance(name, str):
raise ValueError("each variable name must be a string")
if content is None:
content = {}
if not isinstance(content, dict):
raise ValueError(f"the content of '{name}' must be a mapping")

has_default = 'default' in content
description = content.get('description', content.get('help', ''))
normalized.append({
'name': name,
'type': content.get('type', ''),
'default': content.get('default') if has_default else None,
'description': description if description is not None else '',
'required': bool(content.get('required', False)),
})
return normalized

def _print_text(self, structure_definition, variables):
print(f"Variables for {structure_definition}")
if not variables:
print("No variables defined.")
return

rows = [[
variable['name'],
variable['type'] or '-',
self._format_default(variable['default']),
'required' if variable['required'] else 'optional',
variable['description'] or '-',
] for variable in variables]
headers = ['Name', 'Type', 'Default', 'Required', 'Description']
widths = [len(header) for header in headers]
for row in rows:
for index, value in enumerate(row):
widths[index] = max(widths[index], len(value))

print(" " + " ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
print(" " + " ".join("-" * width for width in widths))
for row in rows:
print(" " + " ".join(value.ljust(widths[index]) for index, value in enumerate(row)))

def _format_default(self, value):
if value is None:
return '-'
if isinstance(value, bool):
return str(value).lower()
return str(value)
2 changes: 2 additions & 0 deletions structkit/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from structkit.utils import read_config_file, merge_configs
from structkit.commands.generate import GenerateCommand
from structkit.commands.info import InfoCommand
from structkit.commands.vars import VarsCommand
from structkit.commands.validate import ValidateCommand
from structkit.commands.list import ListCommand
from structkit.commands.search import SearchCommand
Expand Down Expand Up @@ -34,6 +35,7 @@ def get_parser():
InfoCommand(subparsers.add_parser('info', help='Show information about the package'))
ValidateCommand(subparsers.add_parser('validate', help='Validate the YAML configuration file'))
GenerateCommand(subparsers.add_parser('generate', help='Generate the project structure'))
VarsCommand(subparsers.add_parser('vars', help='Inspect structure variables'))
ListCommand(subparsers.add_parser('list', help='List available structures'))
SearchCommand(subparsers.add_parser('search', help='Search available structures by keyword'))
GenerateSchemaCommand(subparsers.add_parser('generate-schema', help='Generate JSON schema for available structures'))
Expand Down
78 changes: 78 additions & 0 deletions structkit/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
2. Getting detailed information about structures
3. Generating structures with various options
4. Validating structure configurations
5. Inspecting structure variables
"""
import asyncio
import logging
Expand All @@ -18,6 +19,7 @@

from structkit.commands.generate import GenerateCommand
from structkit.commands.validate import ValidateCommand
from structkit.commands.vars import VarsCommand
from structkit import __version__


Expand Down Expand Up @@ -193,6 +195,44 @@ class Args:
finally:
sys.stdout = old

def _get_structure_vars_logic(
self,
structure_name: Optional[str],
structures_path: Optional[str] = None,
output: str = "text",
) -> str:
if not structure_name:
return "Error: structure_name is required"

import argparse
from io import StringIO
dummy_parser = argparse.ArgumentParser()
vars_command = VarsCommand(dummy_parser)

config = vars_command._load_yaml_config(structure_name, structures_path)
if config is None:
return f"❗ Structure not found or could not be loaded: {structure_name}"
if not isinstance(config, dict):
return "❗ Invalid structure config: top-level YAML content must be a mapping"

try:
variables = vars_command._normalize_variables(config.get('variables', []))
except ValueError as exc:
return f"❗ Invalid variables config: {exc}"

if output == "json":
import json
return json.dumps(variables, indent=2)

buf = StringIO()
old = sys.stdout
sys.stdout = buf
try:
vars_command._print_text(structure_name, variables)
return buf.getvalue().strip()
finally:
sys.stdout = old

# =====================
# FastMCP tool registration (maps to logic above)
# =====================
Expand All @@ -215,6 +255,25 @@ async def get_structure_info(structure_name: str, structures_path: Optional[str]
self.logger.debug(f"MCP response: get_structure_info len={len(result)} preview=\n{preview}")
return result

@self.app.tool(name="get_structure_vars", description="Inspect variables declared by a specific structure")
async def get_structure_vars(
structure_name: str,
structures_path: Optional[str] = None,
output: str = "text",
) -> str:
self.logger.debug(
"MCP request: get_structure_vars args=%s",
{
"structure_name": structure_name,
"structures_path": structures_path,
"output": output,
},
)
result = self._get_structure_vars_logic(structure_name, structures_path, output)
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
self.logger.debug(f"MCP response: get_structure_vars len={len(result)} preview=\n{preview}")
return result

@self.app.tool(name="generate_structure", description="Generate a project structure using specified definition and options")
async def generate_structure(
structure_definition: str,
Expand Down Expand Up @@ -337,6 +396,25 @@ def __init__(self, content):

return MockResult([MockContent(result_text)])

async def _handle_get_structure_vars(self, params: Dict[str, Any]):
"""Compatibility method for tests that expect MCP-style responses."""
structure_name = params.get('structure_name')
structures_path = params.get('structures_path')
output = params.get('output', 'text')

result_text = self._get_structure_vars_logic(structure_name, structures_path, output)

# Mock MCP response structure
class MockContent:
def __init__(self, text):
self.text = text

class MockResult:
def __init__(self, content):
self.content = content

return MockResult([MockContent(result_text)])


async def main():
logging.basicConfig(level=logging.INFO)
Expand Down
Loading
Loading