vulcai-mcp-forge-cli 0.1.0__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.
Files changed (51) hide show
  1. mcp_forge/__init__.py +107 -0
  2. mcp_forge/cli.py +473 -0
  3. mcp_forge/config_loader.py +60 -0
  4. mcp_forge/discovery/__init__.py +34 -0
  5. mcp_forge/discovery/cli_app.py +294 -0
  6. mcp_forge/discovery/codebase/__init__.py +237 -0
  7. mcp_forge/discovery/codebase/bash.py +179 -0
  8. mcp_forge/discovery/codebase/cobol.py +243 -0
  9. mcp_forge/discovery/codebase/cpp.py +329 -0
  10. mcp_forge/discovery/codebase/csharp.py +243 -0
  11. mcp_forge/discovery/codebase/dart.py +296 -0
  12. mcp_forge/discovery/codebase/elixir.py +210 -0
  13. mcp_forge/discovery/codebase/fortran.py +222 -0
  14. mcp_forge/discovery/codebase/go.py +242 -0
  15. mcp_forge/discovery/codebase/groovy.py +255 -0
  16. mcp_forge/discovery/codebase/java.py +237 -0
  17. mcp_forge/discovery/codebase/js.py +93 -0
  18. mcp_forge/discovery/codebase/julia.py +267 -0
  19. mcp_forge/discovery/codebase/kotlin.py +259 -0
  20. mcp_forge/discovery/codebase/lua.py +94 -0
  21. mcp_forge/discovery/codebase/php.py +239 -0
  22. mcp_forge/discovery/codebase/powershell.py +237 -0
  23. mcp_forge/discovery/codebase/python.py +189 -0
  24. mcp_forge/discovery/codebase/r_lang.py +206 -0
  25. mcp_forge/discovery/codebase/ruby.py +182 -0
  26. mcp_forge/discovery/codebase/rust.py +272 -0
  27. mcp_forge/discovery/codebase/scala.py +250 -0
  28. mcp_forge/discovery/codebase/swift.py +272 -0
  29. mcp_forge/discovery/codebase/typescript.py +268 -0
  30. mcp_forge/discovery/graphql.py +241 -0
  31. mcp_forge/discovery/openapi.py +370 -0
  32. mcp_forge/discovery/web.py +486 -0
  33. mcp_forge/models.py +123 -0
  34. mcp_forge/pipeline/__init__.py +0 -0
  35. mcp_forge/pipeline/local.py +35 -0
  36. mcp_forge/pipeline/remote.py +101 -0
  37. mcp_forge/pipeline/selector.py +34 -0
  38. mcp_forge/schemas/__init__.py +0 -0
  39. mcp_forge/schemas/payload.py +146 -0
  40. mcp_forge/ui/__init__.py +0 -0
  41. mcp_forge/ui/auto.py +107 -0
  42. mcp_forge/ui/guided.py +362 -0
  43. mcp_forge/ui/semi.py +285 -0
  44. mcp_forge/validation/__init__.py +3 -0
  45. mcp_forge/validation/functional_test.py +384 -0
  46. mcp_forge/validation/smoke_test.py +180 -0
  47. mcp_forge/validation/validator.py +109 -0
  48. vulcai_mcp_forge_cli-0.1.0.dist-info/METADATA +271 -0
  49. vulcai_mcp_forge_cli-0.1.0.dist-info/RECORD +51 -0
  50. vulcai_mcp_forge_cli-0.1.0.dist-info/WHEEL +4 -0
  51. vulcai_mcp_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
mcp_forge/__init__.py ADDED
@@ -0,0 +1,107 @@
1
+ """
2
+ mcp-forge — Public Python API
3
+
4
+ Usage::
5
+
6
+ from mcp_forge import generate, run_smoke_test, run_functional_test
7
+ from mcp_forge.models import ForgeConfig, ForgeMode, SourceType, NameStyle
8
+
9
+ config = ForgeConfig(
10
+ source="https://petstore3.swagger.io/api/v3/openapi.json",
11
+ mode=ForgeMode.AUTO,
12
+ output="../mcp-forge-output",
13
+ )
14
+
15
+ generated = generate(config)
16
+ print(f"Server written to: {generated.output_dir}")
17
+ print(f"Tools: {generated.tools_count}")
18
+
19
+ result = run_functional_test(generated.output_dir)
20
+ print(result.summary())
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from mcp_forge.models import (
25
+ DiscoveredTool,
26
+ DiscoveryResult,
27
+ ForgeConfig,
28
+ ForgeMode,
29
+ GeneratedServer,
30
+ NameStyle,
31
+ ParameterType,
32
+ SourceType,
33
+ ToolParameter,
34
+ )
35
+ from mcp_forge.validation.smoke_test import SmokeTestResult, run_smoke_test
36
+ from mcp_forge.validation.functional_test import (
37
+ FunctionalTestResult,
38
+ ToolCallResult,
39
+ run_functional_test,
40
+ )
41
+
42
+ try:
43
+ from importlib.metadata import version as _v
44
+ __version__ = _v("mcp-forge")
45
+ except Exception:
46
+ __version__ = "0.1.0"
47
+
48
+ __all__ = [
49
+ "generate",
50
+ "ForgeConfig",
51
+ "ForgeMode",
52
+ "SourceType",
53
+ "NameStyle",
54
+ "ParameterType",
55
+ "DiscoveredTool",
56
+ "ToolParameter",
57
+ "DiscoveryResult",
58
+ "GeneratedServer",
59
+ "run_smoke_test",
60
+ "SmokeTestResult",
61
+ "run_functional_test",
62
+ "FunctionalTestResult",
63
+ "ToolCallResult",
64
+ ]
65
+
66
+
67
+ def generate(
68
+ config: ForgeConfig,
69
+ *,
70
+ enrich: bool = True,
71
+ ) -> GeneratedServer:
72
+ """
73
+ Generate a MCP server from a source.
74
+
75
+ Args:
76
+ config: ForgeConfig describing the source and output options.
77
+ enrich: Use LLM to improve tool names/descriptions (requires ANTHROPIC_API_KEY).
78
+
79
+ Returns:
80
+ GeneratedServer with output_dir, tools_count and generated file contents.
81
+
82
+ Example::
83
+
84
+ from mcp_forge import generate
85
+ from mcp_forge.models import ForgeConfig, ForgeMode
86
+
87
+ config = ForgeConfig(
88
+ source="https://petstore3.swagger.io/api/v3/openapi.json",
89
+ mode=ForgeMode.AUTO,
90
+ output="../mcp-forge-output",
91
+ )
92
+ server = generate(config, enrich=False)
93
+ print(server.output_dir)
94
+ """
95
+ from mcp_forge.discovery import discover
96
+ from mcp_forge.generation.generator import MCPGenerator
97
+
98
+ result = discover(config)
99
+
100
+ if enrich and not getattr(config, "no_llm", False):
101
+ try:
102
+ from mcp_forge.enrichment.llm import LLMEnricher
103
+ result = LLMEnricher(config).enrich(result)
104
+ except Exception:
105
+ pass
106
+
107
+ return MCPGenerator(config).generate(result)
mcp_forge/cli.py ADDED
@@ -0,0 +1,473 @@
1
+ """
2
+ mcp-forge — Point d'entrée CLI principal.
3
+
4
+ Usage:
5
+ mcp-forge run <source> [options]
6
+ mcp-forge --help
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Annotated, Optional
13
+
14
+ # Dossier output par défaut : dans le répertoire courant de l'utilisateur
15
+ # (Path(__file__) pointe vers site-packages après pip install — on utilise cwd)
16
+ _DEFAULT_OUTPUT = str(Path.cwd().parent / "mcp-forge-output")
17
+
18
+ from dotenv import load_dotenv
19
+ load_dotenv() # Charge automatiquement .env si présent
20
+
21
+ import typer
22
+ from rich import print as rprint
23
+ from rich.console import Console
24
+ from rich.panel import Panel
25
+
26
+ from mcp_forge.config_loader import load_yaml_config
27
+ from mcp_forge.models import ForgeConfig, ForgeMode, NameStyle, SourceType
28
+
29
+ def _version_callback(value: bool) -> None:
30
+ if value:
31
+ from mcp_forge import __version__
32
+ typer.echo(f"mcp-forge {__version__}")
33
+ raise typer.Exit()
34
+
35
+
36
+ app = typer.Typer(
37
+ name="mcp-forge",
38
+ help="Génère automatiquement un serveur MCP à partir d'une application ou d'un site web.",
39
+ add_completion=False,
40
+ rich_markup_mode="rich",
41
+ )
42
+ console = Console()
43
+
44
+
45
+ @app.callback()
46
+ def _main(
47
+ version: Annotated[Optional[bool], typer.Option(
48
+ "--version", "-V", callback=_version_callback, is_eager=True, help="Afficher la version et quitter"
49
+ )] = None,
50
+ ) -> None:
51
+ # Typer callback — gestion de --version uniquement, aucune logique supplémentaire.
52
+ pass
53
+
54
+
55
+ def _detect_source_type(source: str) -> SourceType:
56
+ """Détecte automatiquement le type de source."""
57
+ s = source.lower()
58
+ if s.endswith((".json", ".yaml", ".yml")):
59
+ return SourceType.OPENAPI
60
+ if "graphql" in s or s.endswith("/graphql"):
61
+ return SourceType.GRAPHQL
62
+ if s.startswith("http://") or s.startswith("https://"):
63
+ return SourceType.WEBSITE
64
+ if Path(source).is_dir():
65
+ return SourceType.CODEBASE
66
+ return SourceType.CLI_APP
67
+
68
+
69
+ @app.command()
70
+ def run(
71
+ source: Annotated[Optional[str], typer.Argument(help="URL, fichier OpenAPI, commande CLI ou dossier de code")] = None,
72
+ mode: Annotated[Optional[ForgeMode], typer.Option("--mode", "-m", help="Mode de génération")] = None,
73
+ output: Annotated[Optional[str], typer.Option("--output", "-o", help="Dossier de sortie")] = None,
74
+ source_type: Annotated[Optional[SourceType], typer.Option("--type", "-t", help="Forcer le type de source")] = None,
75
+ name: Annotated[Optional[str], typer.Option("--name", "-n", help="Nom du serveur MCP généré")] = None,
76
+ no_llm: Annotated[Optional[bool], typer.Option("--no-llm", help="Désactiver l'enrichissement par LLM")] = None,
77
+ api_key: Annotated[Optional[str], typer.Option("--api-key", help="Clé API Anthropic (ou via ANTHROPIC_API_KEY)")] = None,
78
+ name_style: Annotated[Optional[NameStyle], typer.Option("--name-style", "-ns", help="Style de nommage : short ou exhaustive")] = None,
79
+ config_file: Annotated[Optional[str], typer.Option("--config", "-c", help="Fichier de configuration YAML (défaut : mcp-forge.yaml si présent)")] = None,
80
+ smoke_test: Annotated[bool, typer.Option("--smoke-test", help="Lancer le smoke test après génération")] = False,
81
+ functional_test: Annotated[bool, typer.Option("--functional-test", help="Lancer le test fonctionnel MCP après génération (démarre le serveur)")] = False,
82
+ local: Annotated[bool, typer.Option("--local", help="Forcer le pipeline local (pas d'appel à l'API Vulcai)")] = False,
83
+ ):
84
+ """
85
+ Lance la génération d'un serveur MCP depuis une [bold]source[/bold].
86
+
87
+ Exemples :
88
+ mcp-forge run https://api.example.com/openapi.json
89
+ mcp-forge run https://example.com --mode semi
90
+ mcp-forge run my-cli-app --mode guided
91
+ """
92
+ console.print(Panel.fit(
93
+ "[bold blue]mcp-forge[/bold blue] — Générateur de serveurs MCP",
94
+ border_style="blue",
95
+ ))
96
+
97
+ yaml_cfg = _load_yaml(config_file)
98
+ source, mode, output, source_type, name, no_llm, name_style, smoke_test, functional_test = \
99
+ _merge_options(yaml_cfg, source, mode, output, source_type, name, no_llm,
100
+ name_style, smoke_test, functional_test)
101
+
102
+ config, no_llm = _build_config(yaml_cfg, source, mode, output, source_type, name,
103
+ no_llm, name_style, api_key, force_local=local)
104
+
105
+ _print_summary(source, config.source_type, mode, output, no_llm, name_style)
106
+
107
+ generated = _dispatch(config, mode)
108
+
109
+ if generated is None or not generated.success:
110
+ raise typer.Exit(1)
111
+
112
+ if smoke_test:
113
+ _run_smoke_test_cmd(generated.output_dir)
114
+ if functional_test:
115
+ _run_functional_test_cmd(generated.output_dir)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Helpers internes — run()
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def _load_yaml(config_file: Optional[str]) -> dict:
123
+ try:
124
+ return load_yaml_config(config_file)
125
+ except FileNotFoundError as e:
126
+ console.print(f"[red][FAIL] {e}[/red]")
127
+ raise typer.Exit(1)
128
+
129
+
130
+ def _merge_options(
131
+ yaml_cfg: dict,
132
+ source: Optional[str],
133
+ mode: Optional[ForgeMode],
134
+ output: Optional[str],
135
+ source_type: Optional[SourceType],
136
+ name: Optional[str],
137
+ no_llm: Optional[bool],
138
+ name_style: Optional[NameStyle],
139
+ smoke_test: bool,
140
+ functional_test: bool,
141
+ ) -> tuple:
142
+ source = source or yaml_cfg.get("source")
143
+ if not source:
144
+ console.print("[red][FAIL] Source requise (argument ou champ 'source' dans mcp-forge.yaml)[/red]")
145
+ raise typer.Exit(1)
146
+
147
+ if not source.startswith("http://") and not source.startswith("https://"):
148
+ source = source.replace("\\", "/")
149
+
150
+ mode = mode or (ForgeMode(yaml_cfg["mode"]) if "mode" in yaml_cfg else ForgeMode.AUTO)
151
+ output = output or yaml_cfg.get("output", _DEFAULT_OUTPUT)
152
+ name = name or yaml_cfg.get("name")
153
+ no_llm = no_llm if no_llm is not None else yaml_cfg.get("no_llm", False)
154
+ smoke_test = smoke_test or yaml_cfg.get("smoke_test", False)
155
+ functional_test = functional_test or yaml_cfg.get("functional_test", False)
156
+ name_style = name_style or (NameStyle(yaml_cfg["name_style"]) if "name_style" in yaml_cfg else NameStyle.SHORT)
157
+ if source_type is None and "type" in yaml_cfg:
158
+ source_type = SourceType(yaml_cfg["type"])
159
+
160
+ return source, mode, output, source_type, name, no_llm, name_style, smoke_test, functional_test
161
+
162
+
163
+ def _build_config(
164
+ yaml_cfg: dict,
165
+ source: str,
166
+ mode: ForgeMode,
167
+ output: str,
168
+ source_type: Optional[SourceType],
169
+ name: Optional[str],
170
+ no_llm: bool,
171
+ name_style: NameStyle,
172
+ api_key: Optional[str],
173
+ force_local: bool = False,
174
+ ) -> tuple:
175
+ resolved_type = source_type or _detect_source_type(source)
176
+ resolved_key = api_key or yaml_cfg.get("api_key") or os.environ.get("ANTHROPIC_API_KEY")
177
+
178
+ if not no_llm and not resolved_key and not os.environ.get("GROQ_API_KEY"):
179
+ console.print(
180
+ "[yellow]⚠ Aucune clé API trouvée.[/yellow] "
181
+ "L'enrichissement LLM sera désactivé. "
182
+ "Définissez ANTHROPIC_API_KEY ou GROQ_API_KEY dans votre .env."
183
+ )
184
+ no_llm = True
185
+
186
+ config = ForgeConfig(
187
+ mode=mode,
188
+ source=source,
189
+ source_type=resolved_type,
190
+ output_dir=output,
191
+ server_name=name or "",
192
+ use_llm_enrichment=not no_llm,
193
+ anthropic_api_key=resolved_key,
194
+ name_style=name_style,
195
+ force_local=force_local,
196
+ )
197
+ return config, no_llm
198
+
199
+
200
+ def _print_summary(
201
+ source: str, resolved_type: SourceType, mode: ForgeMode,
202
+ output: str, no_llm: bool, name_style: NameStyle,
203
+ ) -> None:
204
+ console.print(f"[dim]Source :[/dim] {source}")
205
+ console.print(f"[dim]Type :[/dim] {resolved_type.value}")
206
+ console.print(f"[dim]Mode :[/dim] {mode.value}")
207
+ console.print(f"[dim]Sortie :[/dim] {output}")
208
+ console.print(f"[dim]Enrichiss. :[/dim] {'LLM (Claude)' if not no_llm else 'désactivé'}")
209
+ if not no_llm:
210
+ console.print(f"[dim]Nommage :[/dim] {name_style.value}")
211
+ console.print()
212
+
213
+
214
+ def _dispatch(config: ForgeConfig, mode: ForgeMode):
215
+ if mode == ForgeMode.AUTO:
216
+ from mcp_forge.ui.auto import run_auto
217
+ return run_auto(config)
218
+ if mode == ForgeMode.SEMI:
219
+ from mcp_forge.ui.semi import run_semi
220
+ return run_semi(config)
221
+ if mode == ForgeMode.GUIDED:
222
+ from mcp_forge.ui.guided import run_guided
223
+ return run_guided(config)
224
+ return None
225
+
226
+
227
+ @app.command()
228
+ def test(
229
+ server: Annotated[str, typer.Argument(help="Chemin vers server.py (backslashes acceptés)")],
230
+ ):
231
+ """
232
+ Lance MCP Inspector sur un serveur généré.
233
+
234
+ Accepte les chemins Windows (backslashes) collés depuis l'Explorateur.
235
+
236
+ Exemples :
237
+ mcp-forge test output\\py_requests\\server.py
238
+ mcp-forge test output/py_requests/server.py
239
+ """
240
+ import subprocess
241
+ normalized = server.replace("\\", "/")
242
+ server_path = Path(normalized)
243
+ if not server_path.exists():
244
+ # Essaie relatif au dossier mcp-forge
245
+ server_path = Path(__file__).parent / normalized
246
+ if not server_path.exists():
247
+ console.print(f"[red][FAIL] Fichier introuvable :[/red] {normalized}")
248
+ raise typer.Exit(1)
249
+ server_dir = server_path.resolve().parent
250
+ console.print(f"[dim]Lancement de MCP Inspector sur :[/dim] {server_dir / 'server.py'}")
251
+ # Installe les dépendances si requirements.txt présent (met en cache pour uv)
252
+ req_file = server_dir / "requirements.txt"
253
+ if req_file.exists():
254
+ import sys
255
+ subprocess.run(
256
+ [sys.executable, "-m", "pip", "install", "-q", "-r", str(req_file)],
257
+ cwd=str(server_dir),
258
+ )
259
+ # On se place dans le dossier du serveur pour éviter les espaces dans le chemin
260
+ result = subprocess.run(["mcp", "dev", "server.py"], cwd=str(server_dir))
261
+ if result.returncode != 0:
262
+ raise typer.Exit(result.returncode)
263
+
264
+
265
+ @app.command(name="smoke-test")
266
+ def smoke_test_cmd(
267
+ server_dir: Annotated[str, typer.Argument(help="Dossier du serveur MCP généré (contient server.py)")],
268
+ ):
269
+ """
270
+ Vérifie qu'un serveur MCP généré est valide sans le démarrer.
271
+
272
+ Contrôles effectués :
273
+ • server.py présent
274
+ • Syntaxe Python valide
275
+ • Import sans erreur (subprocess isolé)
276
+ • Objet mcp présent
277
+ • Au moins 1 @mcp.tool enregistré
278
+ • Cohérence avec le fichier VERSION
279
+
280
+ Exemples :
281
+ mcp-forge smoke-test output/my_server
282
+ mcp-forge smoke-test output\\my_server
283
+ """
284
+ normalized = server_dir.replace("\\", "/")
285
+ _run_smoke_test_cmd(normalized)
286
+
287
+
288
+ def _run_smoke_test_cmd(server_dir: str) -> None:
289
+ """Exécute le smoke test et affiche les résultats. Exit 1 si échec."""
290
+ from mcp_forge.validation.smoke_test import run_smoke_test
291
+
292
+ normalized = server_dir.replace("\\", "/")
293
+ path = Path(normalized)
294
+ if not path.exists():
295
+ path = Path(__file__).parent / normalized
296
+ if not path.exists():
297
+ console.print(f"[red][FAIL] Dossier introuvable :[/red] {normalized}")
298
+ raise typer.Exit(1)
299
+
300
+ console.print(f"\n[bold]>> Smoke test :[/bold] {path}")
301
+ result = run_smoke_test(path)
302
+
303
+ for label, ok, detail in result.checks:
304
+ color = "green" if ok else "red"
305
+ mark = "OK " if ok else "FAIL"
306
+ suffix = f" -- {detail}" if detail else ""
307
+ console.print(f" [{color}]{mark}[/{color}] {label}{suffix}")
308
+
309
+ if result.success:
310
+ console.print(f"\n[green bold]Smoke test OK[/green bold] ({result.tools_found} tool(s))")
311
+ else:
312
+ console.print("\n[red bold]Smoke test ECHEC[/red bold]")
313
+ raise typer.Exit(1)
314
+
315
+
316
+ @app.command(name="functional-test")
317
+ def functional_test_cmd(
318
+ server_dir: Annotated[str, typer.Argument(help="Dossier du serveur MCP (contient server.py)")],
319
+ timeout: Annotated[float, typer.Option("--timeout", "-t", help="Délai max en secondes par étape")] = 15.0,
320
+ ):
321
+ """
322
+ Test fonctionnel : démarre le serveur MCP et vérifie ses tools via le protocole MCP.
323
+
324
+ Contrairement au smoke-test (import Python), ce test démarre réellement le serveur
325
+ et communique avec lui via JSON-RPC / stdio — comme un vrai client MCP.
326
+
327
+ Vérifications :
328
+ • Serveur démarre sans erreur
329
+ • Handshake MCP (initialize) réussi
330
+ • tools/list retourne au moins 1 tool
331
+ • Nombre de tools correspond au fichier VERSION
332
+ • Cohérence avec les @mcp.tool du source
333
+
334
+ Exemples :
335
+ mcp-forge functional-test output/my_server
336
+ mcp-forge functional-test output\\my_server --timeout 30
337
+ """
338
+ normalized = server_dir.replace("\\", "/")
339
+ _run_functional_test_cmd(normalized, timeout=timeout)
340
+
341
+
342
+ _FT_STATUS_COLOR = {"ok": "green", "warn": "yellow", "fail": "red", "skip": "dim"}
343
+ _FT_STATUS_MARK = {"ok": "OK ", "warn": "WARN", "fail": "FAIL", "skip": "SKIP"}
344
+
345
+
346
+ def _print_ft_phase1(result) -> None:
347
+ for label, ok, detail in result.checks:
348
+ color = "green" if ok else "red"
349
+ mark = "OK " if ok else "FAIL"
350
+ suffix = f" -- {detail}" if detail else ""
351
+ console.print(f" [{color}]{mark}[/{color}] {label}{suffix}")
352
+
353
+
354
+ def _print_ft_phase2(result) -> None:
355
+ if not result.tool_calls:
356
+ return
357
+ console.print("\n[bold]Phase 2 -- Appel des tools :[/bold]")
358
+ for tc in result.tool_calls:
359
+ color = _FT_STATUS_COLOR.get(tc.status, "white")
360
+ mark = _FT_STATUS_MARK.get(tc.status, "? ")
361
+ args_str = f" args={tc.args_used}" if tc.args_used else ""
362
+ suffix = f" -- {tc.detail}" if tc.detail else ""
363
+ console.print(f" [{color}]{mark}[/{color}] {tc.name}{args_str}{suffix}")
364
+
365
+
366
+ def _run_functional_test_cmd(server_dir: str, timeout: float = 15.0) -> None:
367
+ """Exécute le test fonctionnel MCP et affiche les résultats. Exit 1 si échec."""
368
+ from mcp_forge.validation.functional_test import run_functional_test
369
+
370
+ normalized = server_dir.replace("\\", "/")
371
+ path = Path(normalized)
372
+ if not path.exists():
373
+ path = Path(__file__).parent / normalized
374
+ if not path.exists():
375
+ console.print(f"[red]Dossier introuvable :[/red] {normalized}")
376
+ raise typer.Exit(1)
377
+
378
+ console.print(f"\n[bold]>> Test fonctionnel MCP :[/bold] {path}")
379
+ console.print("[dim] Démarrage du serveur...[/dim]")
380
+ result = run_functional_test(path, timeout=timeout)
381
+
382
+ _print_ft_phase1(result)
383
+ _print_ft_phase2(result)
384
+
385
+ if result.success:
386
+ console.print(f"\n[green bold]Test fonctionnel OK[/green bold] ({len(result.tools_listed)} tool(s))")
387
+ else:
388
+ console.print("\n[red bold]Test fonctionnel ECHEC[/red bold]")
389
+ raise typer.Exit(1)
390
+
391
+
392
+ @app.command(name="list")
393
+ def list_servers(
394
+ output: Annotated[Optional[str], typer.Option("--output", "-o", help="Dossier de sortie à inspecter")] = None,
395
+ ):
396
+ """
397
+ Liste les serveurs MCP générés dans le dossier de sortie.
398
+
399
+ Pour chaque serveur, affiche : nom, date de génération, source, nombre de tools.
400
+ """
401
+ from rich.table import Table
402
+
403
+ output_dir = Path(output or _DEFAULT_OUTPUT)
404
+ if not output_dir.exists():
405
+ console.print(f"[yellow]Dossier introuvable :[/yellow] {output_dir}")
406
+ raise typer.Exit(0)
407
+
408
+ servers = sorted(
409
+ [d for d in output_dir.iterdir() if d.is_dir()],
410
+ key=lambda d: d.stat().st_mtime,
411
+ reverse=True,
412
+ )
413
+
414
+ if not servers:
415
+ console.print(f"[dim]Aucun serveur dans {output_dir}[/dim]")
416
+ raise typer.Exit(0)
417
+
418
+ table = Table(title=f"Serveurs MCP dans {output_dir}", show_lines=False)
419
+ table.add_column("Nom", style="bold cyan", no_wrap=True)
420
+ table.add_column("Tools", justify="right")
421
+ table.add_column("Genere le", style="dim")
422
+ table.add_column("Source", style="dim", overflow="fold")
423
+
424
+ for d in servers:
425
+ version_path = d / "VERSION"
426
+ if version_path.exists():
427
+ data = {}
428
+ for line in version_path.read_text(encoding="utf-8").splitlines():
429
+ if ":" in line:
430
+ k, _, v = line.partition(":")
431
+ data[k.strip()] = v.strip()
432
+ tools = data.get("tools_count", "?")
433
+ generated_at = data.get("generated_at", "?")
434
+ source = data.get("source", "?")
435
+ else:
436
+ tools = "?"
437
+ generated_at = "?"
438
+ source = "[dim](no VERSION file)[/dim]"
439
+
440
+ table.add_row(d.name, str(tools), generated_at, source)
441
+
442
+ console.print(table)
443
+
444
+
445
+ @app.command()
446
+ def info():
447
+ """Affiche des informations sur mcp-forge et les types de sources supportés."""
448
+ rprint(Panel(
449
+ """[bold]Sources supportées :[/bold]
450
+
451
+ [green]openapi[/green] Fichier ou URL OpenAPI/Swagger (.json, .yaml)
452
+ [green]website[/green] Site web ou API REST (URL http/https)
453
+ [green]graphql[/green] API GraphQL (URL avec /graphql)
454
+ [green]cli_app[/green] Application CLI locale
455
+ [green]codebase[/green] Dossier de code source
456
+
457
+ [bold]Modes :[/bold]
458
+
459
+ [blue]auto[/blue] Génération entièrement automatique
460
+ [blue]semi[/blue] Pause pour review des tools et du code généré
461
+ [blue]guided[/blue] Étape par étape avec choix interactif
462
+
463
+ [bold]Variables d'environnement :[/bold]
464
+
465
+ ANTHROPIC_API_KEY Clé API pour l'enrichissement LLM
466
+ """,
467
+ title="[bold blue]mcp-forge[/bold blue] — Informations",
468
+ border_style="blue",
469
+ ))
470
+
471
+
472
+ if __name__ == "__main__":
473
+ app()
@@ -0,0 +1,60 @@
1
+ """
2
+ Chargement de la configuration mcp-forge depuis un fichier YAML.
3
+
4
+ Priorité : flags CLI > mcp-forge.yaml > valeurs par défaut
5
+
6
+ Format supporté (mcp-forge.yaml) :
7
+ source: https://api.example.com/openapi.json
8
+ mode: auto
9
+ output: ./generated
10
+ name: my_server
11
+ type: openapi
12
+ name_style: exhaustive
13
+ no_llm: false
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ _DEFAULT_CONFIG_FILE = "mcp-forge.yaml"
21
+
22
+
23
+ def load_yaml_config(config_path: str | None) -> dict[str, Any]:
24
+ """Charge un fichier YAML de configuration.
25
+
26
+ Si config_path est None, cherche mcp-forge.yaml dans le répertoire courant.
27
+ Retourne un dict vide si aucun fichier trouvé.
28
+ """
29
+ import yaml
30
+
31
+ path: Path | None = None
32
+ if config_path:
33
+ path = Path(config_path)
34
+ if not path.exists():
35
+ raise FileNotFoundError(f"Fichier de configuration introuvable : {config_path}")
36
+ else:
37
+ candidate = Path(_DEFAULT_CONFIG_FILE)
38
+ if candidate.exists():
39
+ path = candidate
40
+
41
+ if path is None:
42
+ return {}
43
+
44
+ with path.open(encoding="utf-8") as f:
45
+ data = yaml.safe_load(f) or {}
46
+
47
+ return data
48
+
49
+
50
+ def merge_config(yaml_cfg: dict[str, Any], **cli_flags: Any) -> dict[str, Any]:
51
+ """Fusionne config YAML et flags CLI. Les flags CLI ont la priorité.
52
+
53
+ Un flag CLI est considéré comme "non défini" si sa valeur est None
54
+ (pour les optionnels) ou correspond à la valeur par défaut Typer.
55
+ """
56
+ merged = dict(yaml_cfg)
57
+ for key, value in cli_flags.items():
58
+ if value is not None:
59
+ merged[key] = value
60
+ return merged
@@ -0,0 +1,34 @@
1
+ """
2
+ Modules de découverte des capacités d'une source.
3
+ """
4
+ from .openapi import OpenAPIDiscovery
5
+ from .web import WebDiscovery
6
+ from .graphql import GraphQLDiscovery
7
+ from .cli_app import CLIAppDiscovery
8
+ from .codebase import CodebaseDiscovery
9
+ from mcp_forge.models import DiscoveryResult, ForgeConfig, SourceType
10
+
11
+
12
+ def discover(config: ForgeConfig) -> DiscoveryResult:
13
+ """Point d'entrée principal : sélectionne et lance le bon module de découverte."""
14
+ discoverers = {
15
+ SourceType.OPENAPI: OpenAPIDiscovery,
16
+ SourceType.WEBSITE: WebDiscovery,
17
+ SourceType.GRAPHQL: GraphQLDiscovery,
18
+ SourceType.CLI_APP: CLIAppDiscovery,
19
+ SourceType.CODEBASE: CodebaseDiscovery,
20
+ }
21
+ cls = discoverers.get(config.source_type)
22
+ if cls is None:
23
+ raise ValueError(f"Type de source non supporté : {config.source_type}")
24
+ return cls(config).discover()
25
+
26
+
27
+ __all__ = [
28
+ "discover",
29
+ "OpenAPIDiscovery",
30
+ "WebDiscovery",
31
+ "GraphQLDiscovery",
32
+ "CLIAppDiscovery",
33
+ "CodebaseDiscovery",
34
+ ]