fips-agents-cli 0.1.0__py3-none-any.whl → 0.1.2__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.
fips_agents_cli/cli.py CHANGED
@@ -4,6 +4,7 @@ import click
4
4
  from rich.console import Console
5
5
 
6
6
  from fips_agents_cli.commands.create import create
7
+ from fips_agents_cli.commands.generate import generate
7
8
  from fips_agents_cli.version import __version__
8
9
 
9
10
  console = Console()
@@ -23,6 +24,7 @@ def cli(ctx):
23
24
 
24
25
  # Register commands
25
26
  cli.add_command(create)
27
+ cli.add_command(generate)
26
28
 
27
29
 
28
30
  def main():
@@ -1,7 +1,6 @@
1
1
  """Create command for generating new projects from templates."""
2
2
 
3
3
  import sys
4
- from typing import Optional
5
4
 
6
5
  import click
7
6
  from rich.console import Console
@@ -43,7 +42,7 @@ def create():
43
42
  default=False,
44
43
  help="Skip git repository initialization",
45
44
  )
46
- def mcp_server(project_name: str, target_dir: Optional[str], no_git: bool):
45
+ def mcp_server(project_name: str, target_dir: str | None, no_git: bool):
47
46
  """
48
47
  Create a new MCP server project from template.
49
48
 
@@ -0,0 +1,405 @@
1
+ """Generate command for scaffolding MCP components in existing projects."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+
12
+ from fips_agents_cli.tools.generators import (
13
+ get_project_info,
14
+ load_params_file,
15
+ load_template,
16
+ render_component,
17
+ run_component_tests,
18
+ validate_python_syntax,
19
+ write_component_file,
20
+ )
21
+ from fips_agents_cli.tools.validation import (
22
+ component_exists,
23
+ find_project_root,
24
+ is_valid_component_name,
25
+ validate_generator_templates,
26
+ )
27
+
28
+ console = Console()
29
+
30
+
31
+ def generate_component_workflow(
32
+ component_type: str,
33
+ name: str,
34
+ template_vars: dict[str, Any],
35
+ params_path: str | None,
36
+ dry_run: bool,
37
+ description: str | None,
38
+ ) -> None:
39
+ """
40
+ Common workflow for generating any component type.
41
+
42
+ Args:
43
+ component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
44
+ name: Component name
45
+ template_vars: Template variables dictionary
46
+ params_path: Optional path to params JSON file
47
+ dry_run: If True, only show what would be generated
48
+ description: Component description
49
+ """
50
+ # Step 1: Find project root
51
+ project_root = find_project_root()
52
+ if not project_root:
53
+ console.print(
54
+ "[red]✗[/red] Not in an MCP server project directory\n"
55
+ "[yellow]Hint:[/yellow] Run this command from within an MCP server project, "
56
+ "or use 'fips-agents create mcp-server' to create one"
57
+ )
58
+ sys.exit(1)
59
+
60
+ console.print(f"[green]✓[/green] Found project root: {project_root}")
61
+
62
+ # Step 2: Validate component name
63
+ is_valid, error_msg = is_valid_component_name(name)
64
+ if not is_valid:
65
+ console.print(f"[red]✗[/red] Invalid component name: {error_msg}")
66
+ sys.exit(1)
67
+
68
+ console.print(f"[green]✓[/green] Component name '{name}' is valid")
69
+
70
+ # Step 3: Check if component already exists
71
+ if component_exists(project_root, component_type, name):
72
+ console.print(
73
+ f"[red]✗[/red] Component '{name}' already exists in src/{component_type}s/\n"
74
+ "[yellow]Hint:[/yellow] Choose a different name or manually remove the existing file"
75
+ )
76
+ sys.exit(1)
77
+
78
+ # Step 4: Validate generator templates
79
+ is_valid, error_msg = validate_generator_templates(project_root, component_type)
80
+ if not is_valid:
81
+ console.print(f"[red]✗[/red] {error_msg}")
82
+ sys.exit(1)
83
+
84
+ console.print(f"[green]✓[/green] Generator templates found for '{component_type}'")
85
+
86
+ # Step 5: Prompt for description if not provided
87
+ if not description:
88
+ description = click.prompt(
89
+ f"Enter a description for the {component_type}",
90
+ type=str,
91
+ default="TODO: Add description",
92
+ )
93
+
94
+ template_vars["description"] = description
95
+ template_vars["component_name"] = name
96
+
97
+ # Step 6: Load params file if provided
98
+ if params_path:
99
+ try:
100
+ params = load_params_file(Path(params_path))
101
+ template_vars["params"] = params
102
+ console.print(f"[green]✓[/green] Loaded {len(params)} parameter(s) from {params_path}")
103
+ except Exception as e:
104
+ console.print(f"[red]✗[/red] Failed to load parameters file: {e}")
105
+ sys.exit(1)
106
+ elif "params" not in template_vars:
107
+ template_vars["params"] = []
108
+
109
+ # Step 7: Get project info
110
+ try:
111
+ project_info = get_project_info(project_root)
112
+ template_vars["project_name"] = project_info["name"]
113
+ except Exception as e:
114
+ console.print(f"[yellow]⚠[/yellow] Could not load project info: {e}")
115
+ template_vars["project_name"] = "unknown"
116
+
117
+ # Step 8: Define file paths
118
+ component_dir_map = {
119
+ "tool": "tools",
120
+ "resource": "resources",
121
+ "prompt": "prompts",
122
+ "middleware": "middleware",
123
+ }
124
+ component_dir = component_dir_map[component_type]
125
+
126
+ component_file = project_root / "src" / component_dir / f"{name}.py"
127
+ test_file = project_root / "tests" / component_dir / f"test_{name}.py"
128
+
129
+ # Step 9: Dry run - show paths and exit
130
+ if dry_run:
131
+ console.print("\n[bold cyan]Dry Run - Files that would be generated:[/bold cyan]\n")
132
+ console.print(f" [cyan]Component:[/cyan] {component_file}")
133
+ console.print(f" [cyan]Test:[/cyan] {test_file}")
134
+ console.print("\n[dim]Template variables:[/dim]")
135
+ for key, value in template_vars.items():
136
+ if key == "params" and isinstance(value, list):
137
+ console.print(f" {key}: {len(value)} parameter(s)")
138
+ else:
139
+ console.print(f" {key}: {value}")
140
+ sys.exit(0)
141
+
142
+ # Step 10: Load and render templates
143
+ with Progress(
144
+ SpinnerColumn(),
145
+ TextColumn("[progress.description]{task.description}"),
146
+ console=console,
147
+ ) as progress:
148
+ progress.add_task(description="Generating component files...", total=None)
149
+
150
+ try:
151
+ # Load templates
152
+ component_template = load_template(project_root, component_type, "component.py.j2")
153
+ test_template = load_template(project_root, component_type, "test.py.j2")
154
+
155
+ # Render templates
156
+ component_code = render_component(component_template, template_vars)
157
+ test_code = render_component(test_template, template_vars)
158
+
159
+ except Exception as e:
160
+ console.print(f"\n[red]✗[/red] Failed to render templates: {e}")
161
+ sys.exit(1)
162
+
163
+ # Step 11: Validate Python syntax
164
+ is_valid, error_msg = validate_python_syntax(component_code)
165
+ if not is_valid:
166
+ console.print(f"[red]✗[/red] Generated component has syntax errors: {error_msg}")
167
+ console.print("[red]This is a bug in the template. Please report this issue.[/red]")
168
+ sys.exit(1)
169
+
170
+ is_valid, error_msg = validate_python_syntax(test_code)
171
+ if not is_valid:
172
+ console.print(f"[red]✗[/red] Generated test has syntax errors: {error_msg}")
173
+ console.print("[red]This is a bug in the template. Please report this issue.[/red]")
174
+ sys.exit(1)
175
+
176
+ console.print("[green]✓[/green] Generated code passed syntax validation")
177
+
178
+ # Step 12: Write files
179
+ try:
180
+ write_component_file(component_code, component_file)
181
+ console.print(f"[green]✓[/green] Created: {component_file.relative_to(project_root)}")
182
+
183
+ write_component_file(test_code, test_file)
184
+ console.print(f"[green]✓[/green] Created: {test_file.relative_to(project_root)}")
185
+
186
+ except Exception as e:
187
+ console.print(f"[red]✗[/red] Failed to write files: {e}")
188
+ sys.exit(1)
189
+
190
+ # Step 13: Run tests
191
+ console.print("\n[cyan]Running generated tests...[/cyan]")
192
+ success, output = run_component_tests(project_root, test_file)
193
+
194
+ if success:
195
+ console.print("[green]✓[/green] Tests passed!\n")
196
+ else:
197
+ console.print("[yellow]⚠[/yellow] Tests failed or had issues:\n")
198
+ console.print(output)
199
+ console.print(
200
+ "\n[yellow]Note:[/yellow] Generated tests are placeholders. "
201
+ "Update them with your actual test cases."
202
+ )
203
+
204
+ # Step 14: Success message
205
+ success_message = f"""
206
+ [bold green]✓ Successfully generated {component_type} component![/bold green]
207
+
208
+ [bold cyan]Files Created:[/bold cyan]
209
+ • {component_file.relative_to(project_root)}
210
+ • {test_file.relative_to(project_root)}
211
+
212
+ [bold cyan]Next Steps:[/bold cyan]
213
+ 1. Review the generated code and implement the business logic
214
+ 2. Update the test file with real test cases
215
+ 3. Run tests: [dim]pytest {test_file.relative_to(project_root)}[/dim]
216
+ 4. Import and use your {component_type} in your MCP server
217
+
218
+ [bold cyan]Implementation Notes:[/bold cyan]
219
+ • Replace TODO comments with actual implementation
220
+ • Remove placeholder return values
221
+ • Add proper error handling
222
+ • Update docstrings as needed
223
+ """
224
+
225
+ console.print(Panel(success_message, border_style="green", padding=(1, 2)))
226
+
227
+
228
+ @click.group()
229
+ def generate():
230
+ """Generate new MCP components in existing projects."""
231
+ pass
232
+
233
+
234
+ @generate.command("tool")
235
+ @click.argument("name")
236
+ @click.option(
237
+ "--async/--sync",
238
+ "is_async",
239
+ default=True,
240
+ help="Generate async or sync function (default: async)",
241
+ )
242
+ @click.option("--with-context", is_flag=True, help="Include FastMCP Context parameter")
243
+ @click.option("--with-auth", is_flag=True, help="Include authentication decorator")
244
+ @click.option("--description", "-d", help="Tool description")
245
+ @click.option("--params", type=click.Path(exists=True), help="JSON file with parameter definitions")
246
+ @click.option(
247
+ "--read-only", is_flag=True, default=True, help="Mark as read-only operation (default: true)"
248
+ )
249
+ @click.option("--idempotent", is_flag=True, default=True, help="Mark as idempotent (default: true)")
250
+ @click.option("--open-world", is_flag=True, help="Mark as open-world operation")
251
+ @click.option("--return-type", default="str", help="Return type annotation (default: str)")
252
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
253
+ def tool(
254
+ name: str,
255
+ is_async: bool,
256
+ with_context: bool,
257
+ with_auth: bool,
258
+ description: str | None,
259
+ params: str | None,
260
+ read_only: bool,
261
+ idempotent: bool,
262
+ open_world: bool,
263
+ return_type: str,
264
+ dry_run: bool,
265
+ ):
266
+ """
267
+ Generate a new tool component.
268
+
269
+ NAME is the tool name in snake_case (e.g., search_documents, fetch_data)
270
+
271
+ Example:
272
+ fips-agents generate tool search_documents --description "Search through documents"
273
+ fips-agents generate tool fetch_data --params params.json --with-context
274
+ """
275
+ console.print("\n[bold cyan]Generating Tool Component[/bold cyan]\n")
276
+
277
+ template_vars = {
278
+ "async": is_async,
279
+ "with_context": with_context,
280
+ "with_auth": with_auth,
281
+ "read_only": read_only,
282
+ "idempotent": idempotent,
283
+ "open_world": open_world,
284
+ "return_type": return_type,
285
+ }
286
+
287
+ generate_component_workflow("tool", name, template_vars, params, dry_run, description)
288
+
289
+
290
+ @generate.command("resource")
291
+ @click.argument("name")
292
+ @click.option(
293
+ "--async/--sync",
294
+ "is_async",
295
+ default=True,
296
+ help="Generate async or sync function (default: async)",
297
+ )
298
+ @click.option("--with-context", is_flag=True, help="Include FastMCP Context parameter")
299
+ @click.option("--description", "-d", help="Resource description")
300
+ @click.option("--uri", help="Resource URI (default: resource://<name>)")
301
+ @click.option(
302
+ "--mime-type", default="text/plain", help="MIME type for resource (default: text/plain)"
303
+ )
304
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
305
+ def resource(
306
+ name: str,
307
+ is_async: bool,
308
+ with_context: bool,
309
+ description: str | None,
310
+ uri: str | None,
311
+ mime_type: str,
312
+ dry_run: bool,
313
+ ):
314
+ """
315
+ Generate a new resource component.
316
+
317
+ NAME is the resource name in snake_case (e.g., config_data, user_profile)
318
+
319
+ Example:
320
+ fips-agents generate resource config_data --description "Application configuration"
321
+ fips-agents generate resource user_profile --uri "resource://users/{id}"
322
+ """
323
+ console.print("\n[bold cyan]Generating Resource Component[/bold cyan]\n")
324
+
325
+ # Default URI if not provided
326
+ if not uri:
327
+ uri = f"resource://{name}"
328
+
329
+ template_vars = {
330
+ "async": is_async,
331
+ "with_context": with_context,
332
+ "uri": uri,
333
+ "mime_type": mime_type,
334
+ "return_type": "str", # Resources typically return strings
335
+ }
336
+
337
+ generate_component_workflow("resource", name, template_vars, None, dry_run, description)
338
+
339
+
340
+ @generate.command("prompt")
341
+ @click.argument("name")
342
+ @click.option("--description", "-d", help="Prompt description")
343
+ @click.option("--params", type=click.Path(exists=True), help="JSON file with parameter definitions")
344
+ @click.option("--with-schema", is_flag=True, help="Include JSON schema in prompt")
345
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
346
+ def prompt(
347
+ name: str,
348
+ description: str | None,
349
+ params: str | None,
350
+ with_schema: bool,
351
+ dry_run: bool,
352
+ ):
353
+ """
354
+ Generate a new prompt component.
355
+
356
+ NAME is the prompt name in snake_case (e.g., code_review, summarize_text)
357
+
358
+ Example:
359
+ fips-agents generate prompt code_review --description "Review code for best practices"
360
+ fips-agents generate prompt summarize_text --params params.json
361
+ """
362
+ console.print("\n[bold cyan]Generating Prompt Component[/bold cyan]\n")
363
+
364
+ template_vars = {
365
+ "with_schema": with_schema,
366
+ "async": False, # Prompts are typically not async
367
+ "return_type": "list[PromptMessage]",
368
+ }
369
+
370
+ generate_component_workflow("prompt", name, template_vars, params, dry_run, description)
371
+
372
+
373
+ @generate.command("middleware")
374
+ @click.argument("name")
375
+ @click.option(
376
+ "--async/--sync",
377
+ "is_async",
378
+ default=True,
379
+ help="Generate async or sync function (default: async)",
380
+ )
381
+ @click.option("--description", "-d", help="Middleware description")
382
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without creating files")
383
+ def middleware(
384
+ name: str,
385
+ is_async: bool,
386
+ description: str | None,
387
+ dry_run: bool,
388
+ ):
389
+ """
390
+ Generate a new middleware component.
391
+
392
+ NAME is the middleware name in snake_case (e.g., auth_middleware, rate_limiter)
393
+
394
+ Example:
395
+ fips-agents generate middleware auth_middleware --description "Authentication middleware"
396
+ fips-agents generate middleware rate_limiter --sync
397
+ """
398
+ console.print("\n[bold cyan]Generating Middleware Component[/bold cyan]\n")
399
+
400
+ template_vars = {
401
+ "async": is_async,
402
+ "return_type": "None", # Middleware typically doesn't return values
403
+ }
404
+
405
+ generate_component_workflow("middleware", name, template_vars, None, dry_run, description)
@@ -1,7 +1,6 @@
1
1
  """Filesystem utilities for project operations."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  from rich.console import Console
7
6
 
@@ -54,7 +53,7 @@ def check_directory_empty(path: Path) -> bool:
54
53
 
55
54
  def validate_target_directory(
56
55
  target_path: Path, allow_existing: bool = False
57
- ) -> tuple[bool, Optional[str]]:
56
+ ) -> tuple[bool, str | None]:
58
57
  """
59
58
  Validate that a target directory is suitable for project creation.
60
59
 
@@ -86,7 +85,7 @@ def validate_target_directory(
86
85
  return True, None
87
86
 
88
87
 
89
- def resolve_target_path(project_name: str, target_dir: Optional[str] = None) -> Path:
88
+ def resolve_target_path(project_name: str, target_dir: str | None = None) -> Path:
90
89
  """
91
90
  Resolve the target path for project creation.
92
91
 
@@ -105,7 +104,7 @@ def resolve_target_path(project_name: str, target_dir: Optional[str] = None) ->
105
104
  return base / project_name
106
105
 
107
106
 
108
- def get_relative_path(path: Path, base: Optional[Path] = None) -> str:
107
+ def get_relative_path(path: Path, base: Path | None = None) -> str:
109
108
  """
110
109
  Get a relative path string for display purposes.
111
110