fips-agents-cli 0.1.0__py3-none-any.whl → 0.1.1__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 +2 -0
- fips_agents_cli/commands/create.py +1 -2
- fips_agents_cli/commands/generate.py +405 -0
- fips_agents_cli/tools/filesystem.py +3 -4
- fips_agents_cli/tools/generators.py +311 -0
- fips_agents_cli/tools/project.py +1 -2
- fips_agents_cli/tools/validation.py +183 -0
- fips_agents_cli/version.py +1 -1
- {fips_agents_cli-0.1.0.dist-info → fips_agents_cli-0.1.1.dist-info}/METADATA +222 -5
- fips_agents_cli-0.1.1.dist-info/RECORD +18 -0
- fips_agents_cli-0.1.0.dist-info/RECORD +0 -15
- {fips_agents_cli-0.1.0.dist-info → fips_agents_cli-0.1.1.dist-info}/WHEEL +0 -0
- {fips_agents_cli-0.1.0.dist-info → fips_agents_cli-0.1.1.dist-info}/entry_points.txt +0 -0
- {fips_agents_cli-0.1.0.dist-info → fips_agents_cli-0.1.1.dist-info}/licenses/LICENSE +0 -0
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:
|
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,
|
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:
|
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:
|
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
|
|
@@ -0,0 +1,311 @@
|
|
1
|
+
"""Generator utilities for rendering MCP component templates."""
|
2
|
+
|
3
|
+
import ast
|
4
|
+
import json
|
5
|
+
import subprocess
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
import jinja2
|
10
|
+
import tomlkit
|
11
|
+
from rich.console import Console
|
12
|
+
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
def get_project_info(project_root: Path) -> dict[str, Any]:
|
17
|
+
"""
|
18
|
+
Extract project metadata from pyproject.toml.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
project_root: Path to the project root directory
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
dict: Project metadata including:
|
25
|
+
- name: Project name
|
26
|
+
- module_name: Module name (with underscores)
|
27
|
+
- version: Project version
|
28
|
+
|
29
|
+
Raises:
|
30
|
+
FileNotFoundError: If pyproject.toml doesn't exist
|
31
|
+
ValueError: If pyproject.toml is malformed
|
32
|
+
|
33
|
+
Example:
|
34
|
+
>>> info = get_project_info(Path("/path/to/project"))
|
35
|
+
>>> print(info["name"])
|
36
|
+
'my-mcp-server'
|
37
|
+
"""
|
38
|
+
pyproject_path = project_root / "pyproject.toml"
|
39
|
+
|
40
|
+
if not pyproject_path.exists():
|
41
|
+
raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}")
|
42
|
+
|
43
|
+
try:
|
44
|
+
with open(pyproject_path) as f:
|
45
|
+
pyproject = tomlkit.parse(f.read())
|
46
|
+
|
47
|
+
project_name = pyproject.get("project", {}).get("name", "unknown")
|
48
|
+
project_version = pyproject.get("project", {}).get("version", "0.1.0")
|
49
|
+
module_name = project_name.replace("-", "_")
|
50
|
+
|
51
|
+
return {
|
52
|
+
"name": project_name,
|
53
|
+
"module_name": module_name,
|
54
|
+
"version": project_version,
|
55
|
+
}
|
56
|
+
|
57
|
+
except Exception as e:
|
58
|
+
raise ValueError(f"Failed to parse pyproject.toml: {e}") from e
|
59
|
+
|
60
|
+
|
61
|
+
def load_template(project_root: Path, component_type: str, template_name: str) -> jinja2.Template:
|
62
|
+
"""
|
63
|
+
Load a Jinja2 template from the project's generator templates.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
project_root: Path to the project root directory
|
67
|
+
component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
|
68
|
+
template_name: Name of the template file (e.g., 'component.py.j2')
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
jinja2.Template: Loaded Jinja2 template
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
FileNotFoundError: If template file doesn't exist
|
75
|
+
jinja2.TemplateError: If template is malformed
|
76
|
+
|
77
|
+
Example:
|
78
|
+
>>> template = load_template(root, "tool", "component.py.j2")
|
79
|
+
>>> rendered = template.render(component_name="my_tool")
|
80
|
+
"""
|
81
|
+
template_path = (
|
82
|
+
project_root / ".fips-agents-cli" / "generators" / component_type / template_name
|
83
|
+
)
|
84
|
+
|
85
|
+
if not template_path.exists():
|
86
|
+
raise FileNotFoundError(f"Template not found: {template_path}")
|
87
|
+
|
88
|
+
try:
|
89
|
+
with open(template_path) as f:
|
90
|
+
template_content = f.read()
|
91
|
+
|
92
|
+
# Create a Jinja2 environment with the template directory as the loader path
|
93
|
+
template_dir = template_path.parent
|
94
|
+
env = jinja2.Environment(
|
95
|
+
loader=jinja2.FileSystemLoader(str(template_dir)),
|
96
|
+
trim_blocks=True,
|
97
|
+
lstrip_blocks=True,
|
98
|
+
)
|
99
|
+
|
100
|
+
return env.from_string(template_content)
|
101
|
+
|
102
|
+
except jinja2.TemplateError as e:
|
103
|
+
raise jinja2.TemplateError(f"Failed to load template {template_name}: {e}") from e
|
104
|
+
|
105
|
+
|
106
|
+
def load_params_file(params_path: Path) -> list[dict[str, Any]]:
|
107
|
+
"""
|
108
|
+
Load and validate parameter definitions from a JSON file.
|
109
|
+
|
110
|
+
Expected schema:
|
111
|
+
[
|
112
|
+
{
|
113
|
+
"name": "query",
|
114
|
+
"type": "str",
|
115
|
+
"description": "Search query",
|
116
|
+
"required": true,
|
117
|
+
"min_length": 1,
|
118
|
+
"max_length": 100
|
119
|
+
}
|
120
|
+
]
|
121
|
+
|
122
|
+
Args:
|
123
|
+
params_path: Path to the JSON parameter file
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
list: List of parameter definition dictionaries
|
127
|
+
|
128
|
+
Raises:
|
129
|
+
FileNotFoundError: If params file doesn't exist
|
130
|
+
ValueError: If JSON is invalid or schema is incorrect
|
131
|
+
|
132
|
+
Example:
|
133
|
+
>>> params = load_params_file(Path("params.json"))
|
134
|
+
>>> print(params[0]["name"])
|
135
|
+
'query'
|
136
|
+
"""
|
137
|
+
if not params_path.exists():
|
138
|
+
raise FileNotFoundError(f"Parameters file not found: {params_path}")
|
139
|
+
|
140
|
+
try:
|
141
|
+
with open(params_path) as f:
|
142
|
+
params = json.load(f)
|
143
|
+
|
144
|
+
except json.JSONDecodeError as e:
|
145
|
+
raise ValueError(f"Invalid JSON in parameters file: {e}") from e
|
146
|
+
|
147
|
+
# Validate schema
|
148
|
+
if not isinstance(params, list):
|
149
|
+
raise ValueError("Parameters file must contain a JSON array of parameter definitions")
|
150
|
+
|
151
|
+
for i, param in enumerate(params):
|
152
|
+
if not isinstance(param, dict):
|
153
|
+
raise ValueError(f"Parameter {i} must be a JSON object")
|
154
|
+
|
155
|
+
# Check required fields
|
156
|
+
required_fields = ["name", "type", "description"]
|
157
|
+
for field in required_fields:
|
158
|
+
if field not in param:
|
159
|
+
raise ValueError(f"Parameter {i} missing required field: {field}")
|
160
|
+
|
161
|
+
# Validate name is a valid Python identifier
|
162
|
+
if not param["name"].isidentifier():
|
163
|
+
raise ValueError(f"Parameter {i} has invalid name: {param['name']}")
|
164
|
+
|
165
|
+
# Validate type
|
166
|
+
valid_types = [
|
167
|
+
"str",
|
168
|
+
"int",
|
169
|
+
"float",
|
170
|
+
"bool",
|
171
|
+
"list[str]",
|
172
|
+
"list[int]",
|
173
|
+
"list[float]",
|
174
|
+
"Optional[str]",
|
175
|
+
"Optional[int]",
|
176
|
+
"Optional[float]",
|
177
|
+
"Optional[bool]",
|
178
|
+
]
|
179
|
+
if param["type"] not in valid_types:
|
180
|
+
raise ValueError(
|
181
|
+
f"Parameter {i} has invalid type: {param['type']}. "
|
182
|
+
f"Valid types: {', '.join(valid_types)}"
|
183
|
+
)
|
184
|
+
|
185
|
+
return params
|
186
|
+
|
187
|
+
|
188
|
+
def render_component(template: jinja2.Template, variables: dict[str, Any]) -> str:
|
189
|
+
"""
|
190
|
+
Render a Jinja2 template with the provided variables.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
template: Jinja2 template object
|
194
|
+
variables: Dictionary of template variables
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
str: Rendered template as a string
|
198
|
+
|
199
|
+
Raises:
|
200
|
+
jinja2.TemplateError: If rendering fails
|
201
|
+
|
202
|
+
Example:
|
203
|
+
>>> template = load_template(root, "tool", "component.py.j2")
|
204
|
+
>>> code = render_component(template, {"component_name": "my_tool"})
|
205
|
+
"""
|
206
|
+
try:
|
207
|
+
return template.render(**variables)
|
208
|
+
except jinja2.TemplateError as e:
|
209
|
+
raise jinja2.TemplateError(f"Failed to render template: {e}") from e
|
210
|
+
|
211
|
+
|
212
|
+
def validate_python_syntax(code: str) -> tuple[bool, str]:
|
213
|
+
"""
|
214
|
+
Validate Python code syntax using ast.parse().
|
215
|
+
|
216
|
+
Args:
|
217
|
+
code: Python code as a string
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
tuple: (is_valid, error_message)
|
221
|
+
is_valid is True if syntax is valid, False otherwise
|
222
|
+
error_message is empty string if valid, otherwise contains error description
|
223
|
+
|
224
|
+
Example:
|
225
|
+
>>> code = "def my_func():\\n return 42"
|
226
|
+
>>> is_valid, msg = validate_python_syntax(code)
|
227
|
+
>>> print(is_valid)
|
228
|
+
True
|
229
|
+
"""
|
230
|
+
try:
|
231
|
+
ast.parse(code)
|
232
|
+
return True, ""
|
233
|
+
except SyntaxError as e:
|
234
|
+
return False, f"Syntax error at line {e.lineno}: {e.msg}"
|
235
|
+
except Exception as e:
|
236
|
+
return False, f"Failed to validate syntax: {e}"
|
237
|
+
|
238
|
+
|
239
|
+
def write_component_file(content: str, file_path: Path) -> None:
|
240
|
+
"""
|
241
|
+
Write component content to a file, creating parent directories if needed.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
content: File content as a string
|
245
|
+
file_path: Path where the file should be written
|
246
|
+
|
247
|
+
Raises:
|
248
|
+
OSError: If file cannot be written (permissions, etc.)
|
249
|
+
|
250
|
+
Example:
|
251
|
+
>>> write_component_file("print('hello')", Path("src/tools/my_tool.py"))
|
252
|
+
"""
|
253
|
+
try:
|
254
|
+
# Create parent directories if they don't exist
|
255
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
256
|
+
|
257
|
+
# Write the file
|
258
|
+
with open(file_path, "w") as f:
|
259
|
+
f.write(content)
|
260
|
+
|
261
|
+
except OSError as e:
|
262
|
+
raise OSError(f"Failed to write file {file_path}: {e}") from e
|
263
|
+
|
264
|
+
|
265
|
+
def run_component_tests(project_root: Path, test_file: Path) -> tuple[bool, str]:
|
266
|
+
"""
|
267
|
+
Run pytest on a generated test file and capture output.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
project_root: Path to the project root directory
|
271
|
+
test_file: Path to the test file to run (relative or absolute)
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
tuple: (success, output)
|
275
|
+
success is True if tests passed, False if failed
|
276
|
+
output is the pytest output as a string
|
277
|
+
|
278
|
+
Example:
|
279
|
+
>>> success, output = run_component_tests(root, Path("tests/tools/test_my_tool.py"))
|
280
|
+
>>> if success:
|
281
|
+
... print("Tests passed!")
|
282
|
+
"""
|
283
|
+
try:
|
284
|
+
# Make test_file relative to project_root if it's absolute
|
285
|
+
if test_file.is_absolute():
|
286
|
+
try:
|
287
|
+
test_file = test_file.relative_to(project_root)
|
288
|
+
except ValueError:
|
289
|
+
# test_file is not relative to project_root, use as-is
|
290
|
+
pass
|
291
|
+
|
292
|
+
# Run pytest with minimal output
|
293
|
+
result = subprocess.run(
|
294
|
+
["pytest", str(test_file), "-v", "--tb=short"],
|
295
|
+
cwd=str(project_root),
|
296
|
+
capture_output=True,
|
297
|
+
text=True,
|
298
|
+
timeout=30,
|
299
|
+
)
|
300
|
+
|
301
|
+
output = result.stdout + result.stderr
|
302
|
+
success = result.returncode == 0
|
303
|
+
|
304
|
+
return success, output
|
305
|
+
|
306
|
+
except subprocess.TimeoutExpired:
|
307
|
+
return False, "Test execution timed out after 30 seconds"
|
308
|
+
except FileNotFoundError:
|
309
|
+
return False, "pytest not found. Install with: pip install pytest"
|
310
|
+
except Exception as e:
|
311
|
+
return False, f"Failed to run tests: {e}"
|
fips_agents_cli/tools/project.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
import re
|
4
4
|
import shutil
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Optional
|
7
6
|
|
8
7
|
import tomlkit
|
9
8
|
from rich.console import Console
|
@@ -11,7 +10,7 @@ from rich.console import Console
|
|
11
10
|
console = Console()
|
12
11
|
|
13
12
|
|
14
|
-
def validate_project_name(name: str) -> tuple[bool,
|
13
|
+
def validate_project_name(name: str) -> tuple[bool, str | None]:
|
15
14
|
"""
|
16
15
|
Validate project name according to Python package naming conventions.
|
17
16
|
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""Validation utilities for MCP component generation."""
|
2
|
+
|
3
|
+
import keyword
|
4
|
+
import re
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import tomlkit
|
8
|
+
from rich.console import Console
|
9
|
+
|
10
|
+
console = Console()
|
11
|
+
|
12
|
+
|
13
|
+
def find_project_root() -> Path | None:
|
14
|
+
"""
|
15
|
+
Find the project root by walking up from current directory.
|
16
|
+
|
17
|
+
Looks for pyproject.toml with fastmcp dependency to identify MCP server projects.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Path: Project root path if found
|
21
|
+
None: If no valid MCP project root is found
|
22
|
+
|
23
|
+
Example:
|
24
|
+
>>> root = find_project_root()
|
25
|
+
>>> if root:
|
26
|
+
... print(f"Found project at {root}")
|
27
|
+
"""
|
28
|
+
current_path = Path.cwd()
|
29
|
+
|
30
|
+
# Walk up the directory tree
|
31
|
+
for parent in [current_path] + list(current_path.parents):
|
32
|
+
pyproject_path = parent / "pyproject.toml"
|
33
|
+
|
34
|
+
if pyproject_path.exists():
|
35
|
+
try:
|
36
|
+
with open(pyproject_path) as f:
|
37
|
+
pyproject = tomlkit.parse(f.read())
|
38
|
+
|
39
|
+
# Check if this is an MCP server project
|
40
|
+
dependencies = pyproject.get("project", {}).get("dependencies", [])
|
41
|
+
|
42
|
+
# Check for fastmcp dependency
|
43
|
+
for dep in dependencies:
|
44
|
+
if isinstance(dep, str) and "fastmcp" in dep.lower():
|
45
|
+
return parent
|
46
|
+
|
47
|
+
except Exception as e:
|
48
|
+
console.print(f"[yellow]⚠[/yellow] Could not parse {pyproject_path}: {e}")
|
49
|
+
continue
|
50
|
+
|
51
|
+
return None
|
52
|
+
|
53
|
+
|
54
|
+
def is_valid_component_name(name: str) -> tuple[bool, str]:
|
55
|
+
"""
|
56
|
+
Validate component name as a valid Python identifier.
|
57
|
+
|
58
|
+
Component names must:
|
59
|
+
- Be valid Python identifiers (snake_case)
|
60
|
+
- Not be Python keywords
|
61
|
+
- Not be empty
|
62
|
+
- Start with a letter or underscore
|
63
|
+
- Contain only letters, numbers, and underscores
|
64
|
+
|
65
|
+
Args:
|
66
|
+
name: The component name to validate
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
tuple: (is_valid, error_message)
|
70
|
+
is_valid is True if valid, False otherwise
|
71
|
+
error_message is empty string if valid, otherwise contains error description
|
72
|
+
|
73
|
+
Examples:
|
74
|
+
>>> is_valid_component_name("my_tool")
|
75
|
+
(True, '')
|
76
|
+
>>> is_valid_component_name("123invalid")
|
77
|
+
(False, 'Component name must start with a letter or underscore')
|
78
|
+
"""
|
79
|
+
if not name:
|
80
|
+
return False, "Component name cannot be empty"
|
81
|
+
|
82
|
+
# Check if it's a valid Python identifier
|
83
|
+
if not name.isidentifier():
|
84
|
+
if name[0].isdigit():
|
85
|
+
return False, "Component name must start with a letter or underscore"
|
86
|
+
return False, (
|
87
|
+
"Component name must be a valid Python identifier (use snake_case: "
|
88
|
+
"letters, numbers, underscores only)"
|
89
|
+
)
|
90
|
+
|
91
|
+
# Check if it's a Python keyword
|
92
|
+
if keyword.iskeyword(name):
|
93
|
+
return False, f"Component name '{name}' is a Python keyword and cannot be used"
|
94
|
+
|
95
|
+
# Recommend snake_case
|
96
|
+
if not re.match(r"^[a-z_][a-z0-9_]*$", name):
|
97
|
+
return False, (
|
98
|
+
"Component name should use snake_case (lowercase letters, numbers, " "underscores only)"
|
99
|
+
)
|
100
|
+
|
101
|
+
return True, ""
|
102
|
+
|
103
|
+
|
104
|
+
def component_exists(project_root: Path, component_type: str, name: str) -> bool:
|
105
|
+
"""
|
106
|
+
Check if a component file already exists.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
project_root: Path to the project root directory
|
110
|
+
component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
|
111
|
+
name: Component name (will check for {name}.py)
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
bool: True if component file exists, False otherwise
|
115
|
+
|
116
|
+
Example:
|
117
|
+
>>> root = Path("/path/to/project")
|
118
|
+
>>> component_exists(root, "tool", "my_tool")
|
119
|
+
False
|
120
|
+
"""
|
121
|
+
# Map component types to their directory locations
|
122
|
+
component_dirs = {
|
123
|
+
"tool": "tools",
|
124
|
+
"resource": "resources",
|
125
|
+
"prompt": "prompts",
|
126
|
+
"middleware": "middleware",
|
127
|
+
}
|
128
|
+
|
129
|
+
if component_type not in component_dirs:
|
130
|
+
return False
|
131
|
+
|
132
|
+
component_dir = component_dirs[component_type]
|
133
|
+
component_file = project_root / "src" / component_dir / f"{name}.py"
|
134
|
+
|
135
|
+
return component_file.exists()
|
136
|
+
|
137
|
+
|
138
|
+
def validate_generator_templates(project_root: Path, component_type: str) -> tuple[bool, str]:
|
139
|
+
"""
|
140
|
+
Validate that generator templates exist for the component type.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
project_root: Path to the project root directory
|
144
|
+
component_type: Type of component ('tool', 'resource', 'prompt', 'middleware')
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
tuple: (is_valid, error_message)
|
148
|
+
is_valid is True if templates exist, False otherwise
|
149
|
+
error_message is empty string if valid, otherwise contains error description
|
150
|
+
|
151
|
+
Example:
|
152
|
+
>>> root = Path("/path/to/project")
|
153
|
+
>>> is_valid, msg = validate_generator_templates(root, "tool")
|
154
|
+
>>> if is_valid:
|
155
|
+
... print("Templates found!")
|
156
|
+
"""
|
157
|
+
generators_dir = project_root / ".fips-agents-cli" / "generators" / component_type
|
158
|
+
|
159
|
+
if not generators_dir.exists():
|
160
|
+
return False, (
|
161
|
+
f"Generator templates not found for '{component_type}'\n"
|
162
|
+
f"Expected: {generators_dir}\n"
|
163
|
+
"Was this project created with fips-agents create mcp-server?"
|
164
|
+
)
|
165
|
+
|
166
|
+
# Check for required template files
|
167
|
+
component_template = generators_dir / "component.py.j2"
|
168
|
+
test_template = generators_dir / "test.py.j2"
|
169
|
+
|
170
|
+
missing_files = []
|
171
|
+
if not component_template.exists():
|
172
|
+
missing_files.append("component.py.j2")
|
173
|
+
if not test_template.exists():
|
174
|
+
missing_files.append("test.py.j2")
|
175
|
+
|
176
|
+
if missing_files:
|
177
|
+
return False, (
|
178
|
+
f"Missing template files for '{component_type}':\n"
|
179
|
+
f" {', '.join(missing_files)}\n"
|
180
|
+
f"Expected location: {generators_dir}"
|
181
|
+
)
|
182
|
+
|
183
|
+
return True, ""
|
fips_agents_cli/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fips-agents-cli
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: CLI tool for creating and managing FIPS-compliant AI agent projects
|
5
5
|
Project-URL: Homepage, https://github.com/rdwj/fips-agents-cli
|
6
6
|
Project-URL: Repository, https://github.com/rdwj/fips-agents-cli
|
@@ -13,15 +13,15 @@ Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
20
19
|
Classifier: Topic :: Software Development :: Code Generators
|
21
20
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
22
|
-
Requires-Python: >=3.
|
21
|
+
Requires-Python: >=3.10
|
23
22
|
Requires-Dist: click>=8.1.0
|
24
23
|
Requires-Dist: gitpython>=3.1.0
|
24
|
+
Requires-Dist: jinja2>=3.1.2
|
25
25
|
Requires-Dist: rich>=13.0.0
|
26
26
|
Requires-Dist: tomlkit>=0.12.0
|
27
27
|
Provides-Extra: dev
|
@@ -40,9 +40,10 @@ A command-line tool for creating and managing FIPS-compliant AI agent projects,
|
|
40
40
|
- 🚀 Quick project scaffolding from templates
|
41
41
|
- 📦 MCP server project generation
|
42
42
|
- 🔧 Automatic project customization
|
43
|
+
- ⚡ Component generation (tools, resources, prompts, middleware)
|
43
44
|
- 🎨 Beautiful CLI output with Rich
|
44
45
|
- ✅ Git repository initialization
|
45
|
-
- 🧪 Comprehensive test coverage
|
46
|
+
- 🧪 Comprehensive test coverage with auto-run
|
46
47
|
|
47
48
|
## Installation
|
48
49
|
|
@@ -99,6 +100,25 @@ fips-agents create mcp-server my-server --target-dir ~/projects
|
|
99
100
|
fips-agents create mcp-server my-server --no-git
|
100
101
|
```
|
101
102
|
|
103
|
+
### Generate components in an existing project
|
104
|
+
|
105
|
+
```bash
|
106
|
+
# Navigate to your MCP server project
|
107
|
+
cd my-mcp-server
|
108
|
+
|
109
|
+
# Generate a new tool
|
110
|
+
fips-agents generate tool search_documents --description "Search through documents"
|
111
|
+
|
112
|
+
# Generate a resource
|
113
|
+
fips-agents generate resource config_data --description "Application configuration"
|
114
|
+
|
115
|
+
# Generate a prompt
|
116
|
+
fips-agents generate prompt code_review --description "Review code for best practices"
|
117
|
+
|
118
|
+
# Generate middleware
|
119
|
+
fips-agents generate middleware auth_middleware --description "Authentication middleware"
|
120
|
+
```
|
121
|
+
|
102
122
|
## Usage
|
103
123
|
|
104
124
|
### Basic Commands
|
@@ -111,6 +131,8 @@ fips-agents --version
|
|
111
131
|
fips-agents --help
|
112
132
|
fips-agents create --help
|
113
133
|
fips-agents create mcp-server --help
|
134
|
+
fips-agents generate --help
|
135
|
+
fips-agents generate tool --help
|
114
136
|
```
|
115
137
|
|
116
138
|
### Create MCP Server
|
@@ -140,6 +162,168 @@ fips-agents create mcp-server my-server -t ~/projects
|
|
140
162
|
fips-agents create mcp-server my-server --no-git
|
141
163
|
```
|
142
164
|
|
165
|
+
### Generate Components
|
166
|
+
|
167
|
+
The `generate` command group allows you to scaffold MCP components (tools, resources, prompts, middleware) in existing MCP server projects.
|
168
|
+
|
169
|
+
**Important**: Run these commands from within your MCP server project directory.
|
170
|
+
|
171
|
+
#### Generate Tool
|
172
|
+
|
173
|
+
```bash
|
174
|
+
fips-agents generate tool <name> [OPTIONS]
|
175
|
+
```
|
176
|
+
|
177
|
+
**Arguments:**
|
178
|
+
- `name`: Tool name in snake_case (e.g., `search_documents`, `fetch_data`)
|
179
|
+
|
180
|
+
**Options:**
|
181
|
+
- `--description, -d TEXT`: Tool description
|
182
|
+
- `--async/--sync`: Generate async or sync function (default: async)
|
183
|
+
- `--with-context`: Include FastMCP Context parameter
|
184
|
+
- `--with-auth`: Include authentication decorator
|
185
|
+
- `--params PATH`: JSON file with parameter definitions
|
186
|
+
- `--read-only`: Mark as read-only operation (default: true)
|
187
|
+
- `--idempotent`: Mark as idempotent (default: true)
|
188
|
+
- `--open-world`: Mark as open-world operation
|
189
|
+
- `--return-type TEXT`: Return type annotation (default: str)
|
190
|
+
- `--dry-run`: Show what would be generated without creating files
|
191
|
+
|
192
|
+
**Examples:**
|
193
|
+
|
194
|
+
```bash
|
195
|
+
# Basic tool generation
|
196
|
+
fips-agents generate tool search_documents --description "Search through documents"
|
197
|
+
|
198
|
+
# Tool with context and authentication
|
199
|
+
fips-agents generate tool fetch_user_data --description "Fetch user data" --with-context --with-auth
|
200
|
+
|
201
|
+
# Tool with parameters from JSON file
|
202
|
+
fips-agents generate tool advanced_search --params params.json
|
203
|
+
|
204
|
+
# Sync tool with custom return type
|
205
|
+
fips-agents generate tool process_data --sync --return-type "dict[str, Any]"
|
206
|
+
|
207
|
+
# Dry run to preview
|
208
|
+
fips-agents generate tool test_tool --description "Test" --dry-run
|
209
|
+
```
|
210
|
+
|
211
|
+
#### Generate Resource
|
212
|
+
|
213
|
+
```bash
|
214
|
+
fips-agents generate resource <name> [OPTIONS]
|
215
|
+
```
|
216
|
+
|
217
|
+
**Arguments:**
|
218
|
+
- `name`: Resource name in snake_case (e.g., `config_data`, `user_profile`)
|
219
|
+
|
220
|
+
**Options:**
|
221
|
+
- `--description, -d TEXT`: Resource description
|
222
|
+
- `--async/--sync`: Generate async or sync function (default: async)
|
223
|
+
- `--with-context`: Include FastMCP Context parameter
|
224
|
+
- `--uri TEXT`: Resource URI (default: `resource://<name>`)
|
225
|
+
- `--mime-type TEXT`: MIME type for resource (default: text/plain)
|
226
|
+
- `--dry-run`: Show what would be generated without creating files
|
227
|
+
|
228
|
+
**Examples:**
|
229
|
+
|
230
|
+
```bash
|
231
|
+
# Basic resource
|
232
|
+
fips-agents generate resource config_data --description "Application configuration"
|
233
|
+
|
234
|
+
# Resource with custom URI
|
235
|
+
fips-agents generate resource user_profile --uri "resource://users/{id}" --description "User profile data"
|
236
|
+
|
237
|
+
# Resource with specific MIME type
|
238
|
+
fips-agents generate resource json_config --mime-type "application/json"
|
239
|
+
```
|
240
|
+
|
241
|
+
#### Generate Prompt
|
242
|
+
|
243
|
+
```bash
|
244
|
+
fips-agents generate prompt <name> [OPTIONS]
|
245
|
+
```
|
246
|
+
|
247
|
+
**Arguments:**
|
248
|
+
- `name`: Prompt name in snake_case (e.g., `code_review`, `summarize_text`)
|
249
|
+
|
250
|
+
**Options:**
|
251
|
+
- `--description, -d TEXT`: Prompt description
|
252
|
+
- `--params PATH`: JSON file with parameter definitions
|
253
|
+
- `--with-schema`: Include JSON schema in prompt
|
254
|
+
- `--dry-run`: Show what would be generated without creating files
|
255
|
+
|
256
|
+
**Examples:**
|
257
|
+
|
258
|
+
```bash
|
259
|
+
# Basic prompt
|
260
|
+
fips-agents generate prompt code_review --description "Review code for best practices"
|
261
|
+
|
262
|
+
# Prompt with parameters
|
263
|
+
fips-agents generate prompt summarize_text --params params.json --with-schema
|
264
|
+
```
|
265
|
+
|
266
|
+
#### Generate Middleware
|
267
|
+
|
268
|
+
```bash
|
269
|
+
fips-agents generate middleware <name> [OPTIONS]
|
270
|
+
```
|
271
|
+
|
272
|
+
**Arguments:**
|
273
|
+
- `name`: Middleware name in snake_case (e.g., `auth_middleware`, `rate_limiter`)
|
274
|
+
|
275
|
+
**Options:**
|
276
|
+
- `--description, -d TEXT`: Middleware description
|
277
|
+
- `--async/--sync`: Generate async or sync function (default: async)
|
278
|
+
- `--dry-run`: Show what would be generated without creating files
|
279
|
+
|
280
|
+
**Examples:**
|
281
|
+
|
282
|
+
```bash
|
283
|
+
# Basic middleware
|
284
|
+
fips-agents generate middleware auth_middleware --description "Authentication middleware"
|
285
|
+
|
286
|
+
# Sync middleware
|
287
|
+
fips-agents generate middleware rate_limiter --sync --description "Rate limiting middleware"
|
288
|
+
```
|
289
|
+
|
290
|
+
#### Parameters JSON Schema
|
291
|
+
|
292
|
+
When using `--params` flag, provide a JSON file with parameter definitions:
|
293
|
+
|
294
|
+
```json
|
295
|
+
[
|
296
|
+
{
|
297
|
+
"name": "query",
|
298
|
+
"type": "str",
|
299
|
+
"description": "Search query",
|
300
|
+
"required": true,
|
301
|
+
"min_length": 1,
|
302
|
+
"max_length": 100
|
303
|
+
},
|
304
|
+
{
|
305
|
+
"name": "limit",
|
306
|
+
"type": "int",
|
307
|
+
"description": "Maximum results to return",
|
308
|
+
"required": false,
|
309
|
+
"default": 10,
|
310
|
+
"ge": 1,
|
311
|
+
"le": 100
|
312
|
+
}
|
313
|
+
]
|
314
|
+
```
|
315
|
+
|
316
|
+
**Supported Types:**
|
317
|
+
- `str`, `int`, `float`, `bool`
|
318
|
+
- `list[str]`, `list[int]`, `list[float]`
|
319
|
+
- `Optional[str]`, `Optional[int]`, `Optional[float]`, `Optional[bool]`
|
320
|
+
|
321
|
+
**Pydantic Field Constraints:**
|
322
|
+
- `min_length`, `max_length` (for strings)
|
323
|
+
- `ge`, `le`, `gt`, `lt` (for numbers)
|
324
|
+
- `pattern` (for regex validation on strings)
|
325
|
+
- `default` (default value when optional)
|
326
|
+
|
143
327
|
## Project Name Requirements
|
144
328
|
|
145
329
|
Project names must follow these rules:
|
@@ -249,7 +433,7 @@ fips-agents-cli/
|
|
249
433
|
|
250
434
|
## Requirements
|
251
435
|
|
252
|
-
- Python 3.
|
436
|
+
- Python 3.10 or higher
|
253
437
|
- Git (for cloning templates and initializing repositories)
|
254
438
|
|
255
439
|
## Dependencies
|
@@ -258,6 +442,7 @@ fips-agents-cli/
|
|
258
442
|
- **rich** (>=13.0.0): Terminal output formatting
|
259
443
|
- **gitpython** (>=3.1.0): Git operations
|
260
444
|
- **tomlkit** (>=0.12.0): TOML file manipulation
|
445
|
+
- **jinja2** (>=3.1.2): Template rendering for component generation
|
261
446
|
|
262
447
|
## Contributing
|
263
448
|
|
@@ -295,6 +480,28 @@ If template cloning fails:
|
|
295
480
|
- Verify the template repository is accessible: https://github.com/rdwj/mcp-server-template
|
296
481
|
- Try again later if GitHub is experiencing issues
|
297
482
|
|
483
|
+
### "Not in an MCP server project directory"
|
484
|
+
|
485
|
+
When using `generate` commands:
|
486
|
+
- Ensure you're running the command from within an MCP server project
|
487
|
+
- Check that `pyproject.toml` exists with `fastmcp` dependency
|
488
|
+
- If the project wasn't created with `fips-agents create mcp-server`, generator templates may be missing
|
489
|
+
|
490
|
+
### "Component already exists"
|
491
|
+
|
492
|
+
If you see this error:
|
493
|
+
- Choose a different component name
|
494
|
+
- Manually remove the existing component file from `src/<component-type>/`
|
495
|
+
- Check the component type directory for existing files
|
496
|
+
|
497
|
+
### Invalid component name
|
498
|
+
|
499
|
+
Component names must:
|
500
|
+
- Be valid Python identifiers (snake_case)
|
501
|
+
- Not be Python keywords (`for`, `class`, etc.)
|
502
|
+
- Start with a letter or underscore
|
503
|
+
- Contain only letters, numbers, and underscores
|
504
|
+
|
298
505
|
## License
|
299
506
|
|
300
507
|
MIT License - see LICENSE file for details
|
@@ -307,6 +514,16 @@ MIT License - see LICENSE file for details
|
|
307
514
|
|
308
515
|
## Changelog
|
309
516
|
|
517
|
+
### Version 0.1.1 (Current)
|
518
|
+
|
519
|
+
- Added `fips-agents generate` command group
|
520
|
+
- Component generation: tools, resources, prompts, middleware
|
521
|
+
- Jinja2-based template rendering
|
522
|
+
- Parameter validation and JSON schema support
|
523
|
+
- Auto-run pytest on generated components
|
524
|
+
- Dry-run mode for previewing changes
|
525
|
+
- Comprehensive error handling and validation
|
526
|
+
|
310
527
|
### Version 0.1.0 (MVP)
|
311
528
|
|
312
529
|
- Initial release
|
@@ -0,0 +1,18 @@
|
|
1
|
+
fips_agents_cli/__init__.py,sha256=Jfo6y6T4HIQ-BeeF89w7F-F4BED63KyCIc1yoFGn9OM,167
|
2
|
+
fips_agents_cli/__main__.py,sha256=rUeQY3jrV6hQVAI2IE0qZCcUnvXDMj5LiCjhlXsc9PQ,130
|
3
|
+
fips_agents_cli/cli.py,sha256=c6ZSIfqZXTAfGc_2sKXanWDuWbxXTUjp4nSbl7yeDig,799
|
4
|
+
fips_agents_cli/version.py,sha256=N1v0FJOEwbpJriPhyukh9-v8PmOs6ppJbMd7pTWGYmc,70
|
5
|
+
fips_agents_cli/commands/__init__.py,sha256=AGxi2-Oc-gKE3sR9F39nIxwnzz-4bcfkkJzaP1qhMvU,40
|
6
|
+
fips_agents_cli/commands/create.py,sha256=wWJZTa2NOoF40lnqIGKXKNcZT5jIZxInGk3CAkUfa3w,6372
|
7
|
+
fips_agents_cli/commands/generate.py,sha256=hoPERkaYGlt-R79PRWnxE6vwNtWfun7SMtJN2zaOIKo,14113
|
8
|
+
fips_agents_cli/tools/__init__.py,sha256=ah4OrYFuyQavuhwguFwFS0o-7ZLGIm5ZWWQK5ZTZZSs,47
|
9
|
+
fips_agents_cli/tools/filesystem.py,sha256=1AOOJtkDSw-pqkuUJyVUbquuczpZBZMT4HLl3qCPB3k,3276
|
10
|
+
fips_agents_cli/tools/generators.py,sha256=bwHKBzWkfJxug4YQXrkRQB_-BxR78kZqe46AjC7haHY,9372
|
11
|
+
fips_agents_cli/tools/git.py,sha256=-z2EO7vNZhN6Vuzh34ZbqNteZE4vNnLG8oysgzhmypk,3042
|
12
|
+
fips_agents_cli/tools/project.py,sha256=vgnctNgdRJJB14uqy7WM0bqpAoPoK_fZu16Io5Rn2hA,5699
|
13
|
+
fips_agents_cli/tools/validation.py,sha256=3947i5UI46BmwBw4F0yLNdkWvkGJig8qH1nJzvQS6RA,5771
|
14
|
+
fips_agents_cli-0.1.1.dist-info/METADATA,sha256=O8D_AVK27NvVPL61BdbWHAwpgbFhz7gsZOuGAZXh2eA,14660
|
15
|
+
fips_agents_cli-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
fips_agents_cli-0.1.1.dist-info/entry_points.txt,sha256=srO4LAGNp6zcB9zuPW1toLGPyLbcsad9YWsfNxgz20s,57
|
17
|
+
fips_agents_cli-0.1.1.dist-info/licenses/LICENSE,sha256=dQJIqi2t9SinZu0yALTYJ8juzosu29KPbjU8WhyboRc,1068
|
18
|
+
fips_agents_cli-0.1.1.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
fips_agents_cli/__init__.py,sha256=Jfo6y6T4HIQ-BeeF89w7F-F4BED63KyCIc1yoFGn9OM,167
|
2
|
-
fips_agents_cli/__main__.py,sha256=rUeQY3jrV6hQVAI2IE0qZCcUnvXDMj5LiCjhlXsc9PQ,130
|
3
|
-
fips_agents_cli/cli.py,sha256=e2QGiAH73pHAjpdf7JdovyNZtc2nyb83LaVaLbLqS5o,718
|
4
|
-
fips_agents_cli/version.py,sha256=EeROn-Cy9mXSPq-OrZ--hVR0oQvwMNqIyAgy4w-YowU,70
|
5
|
-
fips_agents_cli/commands/__init__.py,sha256=AGxi2-Oc-gKE3sR9F39nIxwnzz-4bcfkkJzaP1qhMvU,40
|
6
|
-
fips_agents_cli/commands/create.py,sha256=GvGm8VgsIfqZV_yIXXYzeV-nasLIwuD85_l831YjAho,6403
|
7
|
-
fips_agents_cli/tools/__init__.py,sha256=ah4OrYFuyQavuhwguFwFS0o-7ZLGIm5ZWWQK5ZTZZSs,47
|
8
|
-
fips_agents_cli/tools/filesystem.py,sha256=G9wzHrgQpoMw3z5EslsyEL91VLlUT6Cf5Rt80DtkwOw,3313
|
9
|
-
fips_agents_cli/tools/git.py,sha256=-z2EO7vNZhN6Vuzh34ZbqNteZE4vNnLG8oysgzhmypk,3042
|
10
|
-
fips_agents_cli/tools/project.py,sha256=d_qIhsEzPOM55ONSLzheTozXuyuiEEf9OG4hVXISAbk,5730
|
11
|
-
fips_agents_cli-0.1.0.dist-info/METADATA,sha256=j3-wAEIftpKnjwj4BVKykYPQh4eDXGva_RNIKh55Qiw,8220
|
12
|
-
fips_agents_cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
-
fips_agents_cli-0.1.0.dist-info/entry_points.txt,sha256=srO4LAGNp6zcB9zuPW1toLGPyLbcsad9YWsfNxgz20s,57
|
14
|
-
fips_agents_cli-0.1.0.dist-info/licenses/LICENSE,sha256=dQJIqi2t9SinZu0yALTYJ8juzosu29KPbjU8WhyboRc,1068
|
15
|
-
fips_agents_cli-0.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|