Ali Hmaou commited on
Commit
1b8d07e
·
1 Parent(s): 1dbafb0

Premiere version de MCEPTION :)

Browse files
.gitignore ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+
25
+ # Virtual Environment
26
+ .env
27
+ .venv
28
+ env/
29
+ venv/
30
+ ENV/
31
+ env.bak/
32
+ venv.bak/
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+
38
+ # Local Data & References (Swagger, CSV, etc.)
39
+ data/*
40
+ !data/references/
41
+ !data/uploads/
42
+
43
+ # Ignore contents of data subdirectories but keep gitkeep files
44
+ data/references/*
45
+ !data/references/.gitkeep
46
+
47
+ data/uploads/*
48
+ !data/uploads/.gitkeep
49
+
50
+ # Sensitive Information
51
+ secrets.yaml
README.md CHANGED
@@ -1,13 +0,0 @@
1
- ---
2
- title: Metamcp Proto
3
- emoji: 🏢
4
- colorFrom: red
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.0.0
8
- app_file: app.py
9
- pinned: false
10
- short_description: 'Prototype '
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import gradio as gr
4
+
5
+ # Ajoute le dossier courant au path pour les imports relatifs
6
+ sys.path.append(os.path.dirname(__file__))
7
+
8
+ # Importe l'interface Gradio depuis le serveur
9
+ from src.mcp_server.server import demo
10
+
11
+ if __name__ == "__main__":
12
+ # Lance le serveur (Gradio gère automatiquement le port sur Spaces)
13
+ demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True, show_error=True)
data/references/.gitkeep ADDED
File without changes
data/uploads/.gitkeep ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=5.0.0
2
+ mcp>=1.0.0
3
+ huggingface_hub>=0.26.0
4
+ python-dotenv>=1.0.0
5
+ smolagents[mcp]>=1.0.0
6
+ pandas>=2.0.0
7
+ requests>=2.31.0
src/core/builder/code_generator.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import textwrap
2
+
3
+ class CodeGenerator:
4
+ @staticmethod
5
+ def generate_gradio_app(function_code: str, inputs: dict, output_desc: str) -> str:
6
+ """
7
+ Génère le code complet d'une application Gradio à partir d'un snippet de fonction.
8
+
9
+ Args:
10
+ function_code: Le code source de la fonction principale (ex: def count_r(word): ...)
11
+ inputs: Dict décrivant les inputs (ex: {"word": "text"})
12
+ output_desc: Description de l'output
13
+
14
+ Returns:
15
+ Le code source complet de app.py
16
+ """
17
+
18
+ # Analyse simple pour trouver le nom de la fonction (très naïf pour l'instant)
19
+ # On suppose que le code contient "def nom_fonction("
20
+ import re
21
+ match = re.search(r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", function_code)
22
+ func_name = match.group(1) if match else "main_function"
23
+
24
+ # Mapping des types MCP/JSON vers types Gradio
25
+ # Pour simplifier, on mappe tout sur Text pour l'instant ou on utilise les strings directs
26
+ # TODO: Améliorer le mapping des types
27
+
28
+ # Construction du code
29
+ template = f"""
30
+ import gradio as gr
31
+ import json
32
+
33
+ # --- User Defined Logic ---
34
+ {function_code}
35
+
36
+ # --- Gradio Interface ---
37
+
38
+ # Wrapper pour gérer les types si nécessaire
39
+ def wrapper(*args):
40
+ result = {func_name}(*args)
41
+ return str(result) # Force string output for simplicity
42
+
43
+ # Configuration des inputs Gradio
44
+ # Note: Cette partie est générique pour le MVP.
45
+ # Idéalement, on itère sur 'inputs' pour créer les composants Gradio correspondants.
46
+
47
+ iface = gr.Interface(
48
+ fn=wrapper,
49
+ inputs=[gr.Textbox(label=k) for k in {list(inputs.keys())}],
50
+ outputs=gr.Textbox(label="{output_desc}"),
51
+ title="Meta-MCP Generated Tool",
52
+ description="Auto-generated by Meta-MCP Fractal"
53
+ )
54
+
55
+ if __name__ == "__main__":
56
+ iface.launch(mcp_server=True, show_error=True)
57
+ """
58
+ return textwrap.dedent(template).strip()
59
+
60
+ @staticmethod
61
+ def generate_tool_module(function_code: str, inputs: dict, output_desc: str, tool_name: str) -> str:
62
+ """
63
+ Génère un module Python contenant la logique de l'outil et une factory d'interface.
64
+ Destiné à être placé dans le dossier 'tools/'.
65
+ """
66
+ import re
67
+ match = re.search(r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", function_code)
68
+ func_name = match.group(1) if match else "main_function"
69
+
70
+ template = f"""
71
+ import gradio as gr
72
+ import json
73
+
74
+ # --- User Defined Logic ---
75
+ {function_code}
76
+
77
+ # --- Interface Factory ---
78
+ def create_interface():
79
+ # On utilise directement la fonction utilisateur pour préserver la signature et la docstring
80
+ # Cela permet à Gradio de générer une documentation MCP correcte.
81
+ return gr.Interface(
82
+ fn={func_name},
83
+ inputs=[gr.Textbox(label=k) for k in {list(inputs.keys())}],
84
+ outputs=gr.Textbox(label="{output_desc}"),
85
+ title="{tool_name}",
86
+ description="Auto-generated tool: {tool_name}"
87
+ )
88
+ """
89
+ return textwrap.dedent(template).strip()
90
+
91
+ @staticmethod
92
+ def generate_master_app() -> str:
93
+ """
94
+ Génère le fichier app.py principal.
95
+ Utilise une approche standard avec import dynamique simple.
96
+ """
97
+ template = """
98
+ import gradio as gr
99
+ import os
100
+ import sys
101
+ import importlib
102
+
103
+ # Configuration
104
+ TOOLS_DIR = "tools"
105
+
106
+ # S'assurer que le dossier tools existe et est un package
107
+ if not os.path.exists(TOOLS_DIR):
108
+ os.makedirs(TOOLS_DIR, exist_ok=True)
109
+ with open(os.path.join(TOOLS_DIR, "__init__.py"), "w") as f:
110
+ pass
111
+
112
+ # Ajouter le répertoire courant au path pour que 'import tools.xxx' fonctionne
113
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
114
+
115
+ interfaces = []
116
+ names = []
117
+
118
+ print(f"🚀 Starting Meta-MCP Toolbox...")
119
+ print(f"📂 Scanning '{TOOLS_DIR}' directory...")
120
+
121
+ # Scan et import des outils
122
+ try:
123
+ for filename in sorted(os.listdir(TOOLS_DIR)):
124
+ if filename.endswith(".py") and not filename.startswith("_"):
125
+ module_name = filename[:-3]
126
+ full_module_name = f"{TOOLS_DIR}.{module_name}"
127
+
128
+ try:
129
+ print(f" 👉 Importing {full_module_name}...")
130
+ # Import dynamique standard
131
+ # On utilise reload pour être sûr de prendre la dernière version si redémarrage
132
+ module = importlib.import_module(full_module_name)
133
+ importlib.reload(module)
134
+
135
+ if hasattr(module, "create_interface"):
136
+ # Création de l'interface Gradio pour cet outil
137
+ tool_interface = module.create_interface()
138
+ interfaces.append(tool_interface)
139
+ names.append(module_name)
140
+ print(f" ✅ Loaded {module_name}")
141
+ else:
142
+ print(f" ⚠️ Module {module_name} has no create_interface()")
143
+ except Exception as e:
144
+ print(f" ❌ Error loading {module_name}: {e}")
145
+ import traceback
146
+ traceback.print_exc()
147
+
148
+ except Exception as e:
149
+ print(f"Error scanning tools directory: {e}")
150
+
151
+ # Construction de l'interface finale
152
+ if not interfaces:
153
+ demo = gr.Interface(
154
+ fn=lambda x: "No tools loaded yet. Add a tool via Meta-MCP!",
155
+ inputs="text",
156
+ outputs="text",
157
+ title="Empty Toolbox",
158
+ description="This Space is ready to receive tools."
159
+ )
160
+ else:
161
+ demo = gr.TabbedInterface(interfaces, names)
162
+
163
+ if __name__ == "__main__":
164
+ demo.launch(mcp_server=True, show_error=True)
165
+ """
166
+ return textwrap.dedent(template).strip()
167
+
168
+ @staticmethod
169
+ def generate_mcp_server_code(function_code: str) -> str:
170
+ """
171
+ Génère un serveur MCP (FastMCP) au lieu de Gradio (Future feature).
172
+ """
173
+ pass
src/core/builder/proposal_generator.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from huggingface_hub import InferenceClient
4
+
5
+ class ProposalGenerator:
6
+ def __init__(self):
7
+ self.token = os.environ.get("HF_TOKEN")
8
+ # Client par défaut
9
+ self.client = InferenceClient(token=self.token)
10
+
11
+ def generate_from_description(self, project_name: str, description: str, model: str = "Qwen/Qwen2.5-Coder-32B-Instruct", provider: str = None):
12
+ """
13
+ Génère une proposition de code et de configuration à partir d'une description.
14
+ Utilise chat_completion pour une meilleure compatibilité.
15
+ """
16
+
17
+ # Configuration dynamique du client si nécessaire (ex: changement de provider)
18
+ # Note: InferenceClient est léger, on peut l'instancier à la demande ou utiliser l'existant
19
+ # Si provider est spécifié, on l'utilise. Sinon on laisse HF choisir.
20
+ # "None" string from UI should be converted to None type
21
+ if provider == "None" or provider == "":
22
+ provider = None
23
+
24
+ print(f"🤖 Appel LLM avec Modèle: {model}, Provider: {provider}")
25
+
26
+ client = InferenceClient(model=model, token=self.token, provider=provider)
27
+
28
+ messages = [
29
+ {
30
+ "role": "system",
31
+ "content": """You are an expert Python developer creating a tool for an MCP server via Gradio.
32
+ Your goal is to generate production-ready Python code that is fully typed and documented.
33
+ You MUST return ONLY a valid JSON object."""
34
+ },
35
+ {
36
+ "role": "user",
37
+ "content": f"""Create a tool named '{project_name}' that does the following: {description}
38
+
39
+ Requirements:
40
+ 1. The function MUST have a clear and descriptive docstring (Google style preferred) explaining what it does, its arguments, and its return value. This docstring will be used as the tool description for the LLM.
41
+ 2. The function arguments MUST be fully typed (e.g. `word: str`, `count: int`).
42
+ 3. The function return type MUST be specified (e.g. `-> str`).
43
+ 4. The function name should match '{project_name}' (normalized to python snake_case).
44
+ 5. If the code requires external libraries (like `requests`, `pandas`, `numpy`), list them.
45
+
46
+ Return ONLY a valid JSON object with the following structure:
47
+ {{
48
+ "python_code": "def function_name(arg1: type) -> type:\\n \\"\\"\\"Docstring here...\\"\\"\\"\\n ...",
49
+ "inputs": {{ "arg1": "Description for UI label" }},
50
+ "output_desc": "Description for UI label of the output",
51
+ "requirements": ["requests", "pandas"]
52
+ }}
53
+
54
+ Make sure the python_code is a valid, complete, standalone Python function with all necessary imports inside (e.g. `import requests` inside the function or at top level if compatible).
55
+ If the user provides an API Specification (Swagger/OpenAPI), generate a client function that implements the main operation described.
56
+ Do not use markdown formatting (no ```json). Just the raw JSON string.
57
+ """
58
+ }
59
+ ]
60
+
61
+ try:
62
+ response = client.chat_completion(
63
+ messages,
64
+ max_tokens=1024,
65
+ temperature=0.2,
66
+ stream=False
67
+ )
68
+
69
+ # Extraction du contenu
70
+ content = response.choices[0].message.content.strip()
71
+
72
+ # Nettoyage basique pour extraire le JSON si le modèle bavarde un peu
73
+ if content.startswith("```json"):
74
+ content = content[7:]
75
+ elif content.startswith("```"):
76
+ content = content[3:]
77
+ if content.endswith("```"):
78
+ content = content[:-3]
79
+
80
+ return json.loads(content.strip())
81
+
82
+ except Exception as e:
83
+ print(f"Error generating proposal: {e}")
84
+ import traceback
85
+ traceback.print_exc()
86
+ # Fallback en cas d'erreur
87
+ return {
88
+ "python_code": f"# Error generating code: {str(e)}\n# Try changing the Inference Provider or Model.\ndef {project_name.replace('-', '_')}():\n return 'Error'",
89
+ "inputs": {},
90
+ "output_desc": "Error fallback"
91
+ }
92
+
93
+ # Singleton
94
+ proposal_generator = ProposalGenerator()
src/core/builder/reference_parser.py ADDED
File without changes
src/core/deployer/huggingface.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, Optional
3
+ from huggingface_hub import HfApi, get_token
4
+
5
+ class HFDeployer:
6
+ def __init__(self, token: Optional[str] = None):
7
+ """
8
+ Initialise le déployeur Hugging Face.
9
+ Si token est None, essaie de le récupérer depuis l'environnement HF_TOKEN
10
+ ou le cache local.
11
+ """
12
+ self.token = token or os.environ.get("HF_TOKEN") or get_token()
13
+ if not self.token:
14
+ raise ValueError("Aucun token Hugging Face trouvé. Veuillez définir HF_TOKEN.")
15
+
16
+ self.api = HfApi(token=self.token)
17
+
18
+ def _sanitize_repo_id(self, input_name: str, current_username: str) -> str:
19
+ """Nettoie le nom du repo/space pour gérer les URLs et les formats partiels."""
20
+ input_name = input_name.strip()
21
+
22
+ # Cas URL complète : https://huggingface.co/spaces/user/repo
23
+ if "huggingface.co" in input_name:
24
+ parts = input_name.split("huggingface.co/")
25
+ if len(parts) > 1:
26
+ path = parts[1]
27
+ # Retire 'spaces/' si présent
28
+ if path.startswith("spaces/"):
29
+ path = path[7:]
30
+ # Retire le slash final
31
+ return path.rstrip("/")
32
+
33
+ # Cas user/repo
34
+ if "/" in input_name:
35
+ return input_name
36
+
37
+ # Cas repo seul -> user/repo
38
+ return f"{current_username}/{input_name}"
39
+
40
+ def deploy_space(self,
41
+ space_name: str,
42
+ files: Dict[str, str],
43
+ username: Optional[str] = None,
44
+ sdk: str = "gradio",
45
+ private: bool = False) -> str:
46
+ """
47
+ Crée un Space et déploie les fichiers.
48
+
49
+ Args:
50
+ space_name: Nom du space (ex: 'strawberry-counter')
51
+ files: Dictionnaire {nom_fichier: contenu} (ex: {'app.py': '...'})
52
+ username: Nom d'utilisateur ou organisation cible. Si None, utilise l'utilisateur courant.
53
+ sdk: 'gradio', 'streamlit', ou 'docker'
54
+ private: Si True, crée un repo privé
55
+
56
+ Returns:
57
+ L'URL du Space déployé.
58
+ """
59
+
60
+ # 1. Déterminer le repo_id complet
61
+ if not username:
62
+ user_info = self.api.whoami()
63
+ username = user_info["name"]
64
+
65
+ # Utilisation de la méthode de nettoyage
66
+ repo_id = self._sanitize_repo_id(space_name, username)
67
+
68
+ print(f"🚀 Préparation du déploiement vers {repo_id}...")
69
+
70
+ # 2. Création du repo (idempotent: ne plante pas s'il existe déjà)
71
+ try:
72
+ self.api.create_repo(
73
+ repo_id=repo_id,
74
+ repo_type="space",
75
+ space_sdk=sdk,
76
+ private=private,
77
+ exist_ok=True
78
+ )
79
+ print(f"✅ Repo {repo_id} prêt.")
80
+ except Exception as e:
81
+ raise RuntimeError(f"Erreur lors de la création du repo: {str(e)}")
82
+
83
+ # 3. Upload des fichiers
84
+ operations = []
85
+ for filename, content in files.items():
86
+ # On encode le contenu en bytes pour l'upload
87
+ content_bytes = content.encode("utf-8")
88
+ operations.append(
89
+ self.api.run_as_future(
90
+ self.api.upload_file,
91
+ path_or_fileobj=content_bytes,
92
+ path_in_repo=filename,
93
+ repo_id=repo_id,
94
+ repo_type="space"
95
+ )
96
+ )
97
+
98
+ # Note: Pour simplifier ici on fait séquentiel ou on utilise upload_file direct.
99
+ # Pour un vrai batch, commit_operation serait mieux, mais upload_file est simple pour démarrer.
100
+ # Re-implémentation propre avec upload_file direct pour éviter complexité async pour l'instant
101
+
102
+ try:
103
+ for filename, content in files.items():
104
+ print(f"📤 Upload de {filename}...")
105
+ content_bytes = content.encode("utf-8")
106
+ self.api.upload_file(
107
+ path_or_fileobj=content_bytes,
108
+ path_in_repo=filename,
109
+ repo_id=repo_id,
110
+ repo_type="space",
111
+ commit_message=f"Deploy {filename} via Meta-MCP"
112
+ )
113
+ print("✅ Tous les fichiers ont été uploadés.")
114
+ except Exception as e:
115
+ raise RuntimeError(f"Erreur lors de l'upload des fichiers: {str(e)}")
116
+
117
+ # 4. Construction de l'URL
118
+ # L'URL standard est https://huggingface.co/spaces/USERNAME/SPACE_NAME
119
+ space_url = f"https://huggingface.co/spaces/{repo_id}"
120
+
121
+ print(f"🎉 Déploiement terminé ! Space accessible ici : {space_url}")
122
+ return space_url
src/core/security/sandboxing.py ADDED
File without changes
src/core/security/validation.py ADDED
File without changes
src/core/state/session_manager.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from typing import Dict, Any, Optional
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+
6
+ @dataclass
7
+ class ProjectDraft:
8
+ draft_id: str
9
+ name: str
10
+ description: str
11
+ type: str
12
+ created_at: datetime = field(default_factory=datetime.now)
13
+ code_files: Dict[str, str] = field(default_factory=dict)
14
+ metadata: Dict[str, Any] = field(default_factory=dict)
15
+
16
+ class SessionManager:
17
+ def __init__(self):
18
+ self._drafts: Dict[str, ProjectDraft] = {}
19
+
20
+ def create_draft(self, name: str, description: str, type: str = "adhoc") -> ProjectDraft:
21
+ """Crée un nouveau brouillon de projet."""
22
+ draft_id = str(uuid.uuid4())
23
+ draft = ProjectDraft(
24
+ draft_id=draft_id,
25
+ name=name,
26
+ description=description,
27
+ type=type
28
+ )
29
+ # Initialisation des fichiers de base
30
+ draft.code_files["requirements.txt"] = "mcp"
31
+
32
+ self._drafts[draft_id] = draft
33
+ return draft
34
+
35
+ def get_draft(self, draft_id: str) -> Optional[ProjectDraft]:
36
+ """Récupère un brouillon par son ID."""
37
+ return self._drafts.get(draft_id)
38
+
39
+ def update_code(self, draft_id: str, filename: str, content: str) -> bool:
40
+ """Met à jour un fichier de code dans le brouillon."""
41
+ draft = self.get_draft(draft_id)
42
+ if not draft:
43
+ return False
44
+ draft.code_files[filename] = content
45
+ return True
46
+
47
+ def list_drafts(self) -> Dict[str, str]:
48
+ """Liste tous les brouillons actifs."""
49
+ return {d.draft_id: d.name for d in self._drafts.values()}
src/mcp_server/playground.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import io
4
+ import re
5
+ import pandas as pd
6
+ import gradio as gr
7
+ from contextlib import redirect_stdout
8
+ from smolagents import InferenceClientModel, CodeAgent, Tool
9
+
10
+ def remove_ansi_codes(text):
11
+ """Supprime les codes d'échappement ANSI (couleurs) du texte."""
12
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
13
+ return ansi_escape.sub('', text)
14
+
15
+ # Note: MCPClient n'est peut-être pas exposé directement par smolagents dans toutes les versions.
16
+ # Si l'import échoue, il faudra peut-être utiliser une approche différente ou vérifier la version.
17
+ # L'utilisateur a fourni `from smolagents import ..., MCPClient`, donc on suit cette voie.
18
+ try:
19
+ from smolagents import MCPClient
20
+ except ImportError:
21
+ # Fallback ou mock si MCPClient n'est pas encore dans la version installée
22
+ # Pour l'instant on assume que c'est bon comme demandé par l'user
23
+ MCPClient = None
24
+
25
+ class PlaygroundManager:
26
+ def __init__(self):
27
+ self.agent = None
28
+ self.tools = []
29
+ self.mcp_client = None
30
+
31
+ def load_mcp_tools(self, mcp_url: str):
32
+ """Connecte le client MCP à l'URL donnée et charge les outils."""
33
+ try:
34
+ # Nettoyage de l'ancien client
35
+ if self.mcp_client:
36
+ # self.mcp_client.disconnect() # Si méthode existe
37
+ pass
38
+
39
+ # Initialisation du client MCP
40
+ # L'utilisateur a demandé d'ignorer le mode SSE et d'utiliser HTTP streamable
41
+ # On nettoie l'URL si elle contient encore /sse par erreur
42
+ if mcp_url.endswith("/sse"):
43
+ mcp_url = mcp_url[:-4]
44
+
45
+ # On passe l'URL sans forcer le transport SSE, smolagents devrait gérer
46
+ # Note: On passe l'URL directement si possible, ou dans un dict selon l'API
47
+ # structured_output=False pour éviter le FutureWarning et rester compatible
48
+ self.mcp_client = MCPClient({"url": mcp_url}, structured_output=False)
49
+
50
+ # Récupération des outils
51
+ self.tools = self.mcp_client.get_tools()
52
+
53
+ # Configuration de l'agent
54
+ # On utilise HF_TOKEN pour le modèle d'inférence
55
+ token = os.environ.get("HF_TOKEN")
56
+ if not token:
57
+ return pd.DataFrame({"Error": ["HF_TOKEN env var is missing"]}), "Error: HF_TOKEN missing"
58
+
59
+ model = InferenceClientModel(token=token)
60
+ self.agent = CodeAgent(tools=self.tools, model=model)
61
+
62
+ # Création du DataFrame pour l'affichage
63
+ rows = []
64
+ for tool in self.tools:
65
+ # Gestion simplifiée des inputs pour l'affichage
66
+ input_desc = str(tool.inputs) if hasattr(tool, 'inputs') else "N/A"
67
+ rows.append({
68
+ "Tool name": tool.name,
69
+ "Description": tool.description,
70
+ "Params": input_desc
71
+ })
72
+
73
+ df = pd.DataFrame(rows)
74
+ return df, f"Succès ! {len(self.tools)} outils chargés depuis {mcp_url}"
75
+
76
+ except Exception as e:
77
+ import traceback
78
+ traceback.print_exc()
79
+ return pd.DataFrame({"Error": [str(e)]}), f"Erreur de connexion: {str(e)}"
80
+
81
+ def chat(self, message: str, history: list):
82
+ """Exécute le message utilisateur via l'agent en capturant la réflexion."""
83
+ if not self.agent:
84
+ return "⚠️ Veuillez d'abord charger un serveur MCP valide."
85
+
86
+ # Capture de la sortie standard (logs de réflexion de smolagents)
87
+ f = io.StringIO()
88
+ try:
89
+ with redirect_stdout(f):
90
+ # L'agent smolagents s'exécute
91
+ # Note: Le streaming réel de la réflexion nécessiterait une intégration plus profonde avec smolagents
92
+ response = self.agent.run(message)
93
+
94
+ # Nettoyage des logs (suppression des couleurs ANSI qui cassent le Markdown)
95
+ raw_logs = f.getvalue()
96
+ clean_logs = remove_ansi_codes(raw_logs)
97
+
98
+ # Formatage de la réponse avec les logs de réflexion nettoyés
99
+ if clean_logs:
100
+ formatted_response = f"**💭 Réflexion de l'agent :**\n```text\n{clean_logs}\n```\n\n**✅ Réponse :**\n{str(response)}"
101
+ else:
102
+ formatted_response = str(response)
103
+
104
+ return formatted_response
105
+
106
+ except Exception as e:
107
+ raw_logs = f.getvalue()
108
+ clean_logs = remove_ansi_codes(raw_logs)
109
+ return f"Erreur lors de l'exécution de l'agent: {str(e)}\n\nLogs partiels:\n{clean_logs}"
110
+
111
+ # Singleton pour gérer l'état du playground dans l'instance Gradio
112
+ # Attention: Dans un vrai déploiement multi-utilisateurs, l'état devrait être géré par gr.State
113
+ playground = PlaygroundManager()
114
+
115
+ def get_playground_ui_handlers():
116
+ """Retourne les fonctions wrappers pour l'UI Gradio."""
117
+
118
+ def reload_tools(url):
119
+ return playground.load_mcp_tools(url)
120
+
121
+ def chat_response(message, history):
122
+ return playground.chat(message, history)
123
+
124
+ return reload_tools, chat_response
src/mcp_server/server.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import sys
4
+
5
+ # Ajout du répertoire racine au path pour permettre les imports absolus 'src.xxx'
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
7
+
8
+ from src.mcp_server import tools
9
+ from src.mcp_server.playground import get_playground_ui_handlers
10
+ from src.core.builder.proposal_generator import proposal_generator
11
+
12
+ # --- Wrappers pour Gradio UI ---
13
+ # Ces wrappers permettent d'avoir une UI conviviale tout en exposant les fonctions via MCP
14
+
15
+ def init_and_propose_ui(project_name, description, type, model_id, provider_id):
16
+ """
17
+ Step 1 (Initialization): Starts a new tool project and uses AI to propose code.
18
+
19
+ This is the entry point for creating a new MCP tool. It returns a draft_id and a code proposal based on the description.
20
+
21
+ Args:
22
+ project_name: The technical name of the tool (e.g., 'weather-fetcher').
23
+ description: A natural language description of what the tool should do, or a raw Swagger/OpenAPI JSON specification.
24
+ type: The type of tool pattern (e.g., 'adhoc' for custom logic, 'api_wrapper' for REST clients).
25
+ model_id: The LLM model to use for code generation (default: Qwen/Qwen2.5-Coder-32B-Instruct).
26
+ provider_id: The inference provider to use (optional, e.g. 'together', 'fal-ai').
27
+ """
28
+ # 1. Initialisation du projet
29
+ init_result = tools.init_project(project_name, description, type)
30
+ draft_id = init_result.get("draft_id", "")
31
+
32
+ # 2. Génération de la proposition par LLM
33
+ print(f"🤖 Génération de la proposition pour : {project_name} (Model: {model_id}, Provider: {provider_id})...")
34
+ proposal = proposal_generator.generate_from_description(project_name, description, model=model_id, provider=provider_id)
35
+
36
+ # 3. Retourne les données pour mettre à jour l'UI
37
+ # Gère le cas où 'requirements' n'est pas renvoyé par le LLM
38
+ reqs = proposal.get("requirements", [])
39
+
40
+ return (
41
+ init_result, # out_init (JSON)
42
+ draft_id, # draft_id_logic (Textbox)
43
+ proposal["python_code"], # python_code (Code)
44
+ proposal["inputs"], # inputs_dict (JSON)
45
+ proposal["output_desc"], # output_desc (Textbox)
46
+ reqs # requirements_box (JSON/List)
47
+ )
48
+
49
+ def define_logic_ui(draft_id, python_code, inputs, output_desc, requirements):
50
+ """
51
+ Step 2 (Logic Definition): Validates and saves the tool code.
52
+
53
+ Call this AFTER `init_and_propose_ui`. It saves the Python implementation into the draft before deployment.
54
+
55
+ Args:
56
+ draft_id: The unique ID of the project draft (returned by Step 1).
57
+ python_code: The complete Python source code for the tool function.
58
+ inputs: A dictionary describing the input parameters (e.g. {"city": "Name of the city"}).
59
+ output_desc: A description of what the tool returns.
60
+ requirements: A list of Python dependencies (pip packages) required by the code (e.g. ["requests", "pandas"]).
61
+ """
62
+ # inputs est reçu comme un dictionnaire (via gr.JSON)
63
+ result = tools.define_logic(draft_id, python_code, inputs, output_desc, requirements)
64
+ return result
65
+
66
+ def deploy_to_space_ui(draft_id, visibility, space_target, target_space_name):
67
+ """
68
+ Step 3 (Deployment): Deploys the tool to a Hugging Face Space.
69
+
70
+ Call this AFTER `define_logic_ui`. It creates or updates a Space with the tool's code.
71
+
72
+ Args:
73
+ draft_id: The unique ID of the project draft (from Step 1).
74
+ visibility: The visibility of the deployed Space ('public' or 'private').
75
+ space_target: Deployment strategy. 'new' creates a dedicated Space (Toolbox), 'existing' adds the tool to an existing Toolbox Space.
76
+ target_space_name: The name of the target Space. Required if space_target='existing'. Optional for 'new' (defaults to project name).
77
+ """
78
+ result = tools.deploy_to_space(draft_id, visibility, space_target, target_space_name)
79
+ return result
80
+
81
+ # Récupération des handlers du playground
82
+ reload_tools_handler, chat_response_handler = get_playground_ui_handlers()
83
+
84
+ # --- Exposition des outils MCP (API pure) ---
85
+ # Ces fonctions sont exposées directement aux LLMs via MCP, en plus de l'UI
86
+
87
+ def mcp_propose_implementation(project_name: str, description: str):
88
+ """
89
+ [AI Assistant Only] Generates a Python implementation proposal without initializing a UI draft.
90
+
91
+ Use this tool if you are an AI agent wanting to generate code from a spec before deciding to create a draft.
92
+
93
+ Args:
94
+ project_name: Name of the intended tool.
95
+ description: The tool description or Swagger/OpenAPI specification.
96
+ """
97
+ return tools.propose_implementation(project_name, description)
98
+
99
+ # --- Construction de l'interface ---
100
+
101
+ with gr.Blocks(title="Meta-MCP Fractal") as demo:
102
+ gr.Markdown("# 🏭 Méta-MCP Fractal Factory")
103
+ gr.Markdown("Ce serveur permet de créer et déployer d'autres serveurs MCP sur Hugging Face Spaces.")
104
+
105
+ with gr.Tab("1. Initialisation"):
106
+ gr.Markdown("Commencez par initialiser un nouveau projet.")
107
+
108
+ project_name = gr.Textbox(label="Nom du projet (ex: strawberry-counter, ratp-api-client)")
109
+
110
+ project_desc = gr.Textbox(
111
+ label="Description de l'outil ou Spécification (Swagger/OpenAPI JSON)",
112
+ lines=10,
113
+ placeholder="Décrivez ce que doit faire l'outil, ou collez ici le contenu d'un fichier swagger.json pour générer un client API automatiquement."
114
+ )
115
+
116
+ with gr.Row():
117
+ project_type = gr.Dropdown(choices=["adhoc", "api_wrapper"], value="adhoc", label="Type")
118
+
119
+ with gr.Accordion("Paramètres IA (Avancé)", open=False):
120
+ model_id = gr.Textbox(label="Modèle LLM", value="Qwen/Qwen2.5-Coder-32B-Instruct")
121
+ provider_id = gr.Dropdown(
122
+ label="Provider d'Inférence",
123
+ choices=["None", "together", "fal-ai", "replicate", "sambanova", "hyperbolic"],
124
+ value="None",
125
+ info="Sélectionnez un provider spécifique si 'None' (auto) échoue."
126
+ )
127
+
128
+ btn_init = gr.Button("Initialiser le projet & Générer le code (IA)")
129
+ out_init = gr.JSON(label="Résultat (Copiez le draft_id)")
130
+
131
+
132
+ with gr.Tab("2. Définition de la logique"):
133
+ gr.Markdown("Définissez le code Python et l'interface de votre outil.")
134
+ with gr.Row():
135
+ draft_id_logic = gr.Textbox(label="Draft ID")
136
+ python_code = gr.Code(language="python", label="Code Python (ex: def count_r(word): ...)")
137
+
138
+ with gr.Row():
139
+ inputs_dict = gr.JSON(label="Inputs (ex: {'word': 'text'})", value={"word": "text"})
140
+ output_desc = gr.Textbox(label="Description de la sortie")
141
+
142
+ requirements_box = gr.JSON(label="Requirements (Pip packages)", value=[])
143
+
144
+ btn_logic = gr.Button("Générer le code")
145
+ out_logic = gr.JSON(label="Résultat")
146
+
147
+ btn_logic.click(define_logic_ui, inputs=[draft_id_logic, python_code, inputs_dict, output_desc, requirements_box], outputs=out_logic)
148
+
149
+ with gr.Tab("3. Déploiement"):
150
+ gr.Markdown("Déployez votre outil sur Hugging Face Spaces.")
151
+ with gr.Row():
152
+ draft_id_deploy = gr.Textbox(label="Draft ID")
153
+ visibility = gr.Dropdown(choices=["public", "private"], value="public", label="Visibilité")
154
+
155
+ gr.Markdown("---")
156
+ gr.Markdown("### 🎯 Cible du déploiement")
157
+
158
+ with gr.Row():
159
+ space_target = gr.Radio(
160
+ choices=["new", "existing"],
161
+ value="new",
162
+ label="Mode de déploiement",
163
+ info="Choisissez si vous créez une nouvelle Toolbox ou si vous enrichissez une existante."
164
+ )
165
+
166
+ # Ce champ sert pour les deux cas : soit pour nommer la nouvelle toolbox, soit pour cibler l'existante
167
+ target_space_name = gr.Textbox(
168
+ label="Nom du Space Cible",
169
+ placeholder="Laissez vide pour utiliser le nom du projet, ou saisissez un nom (ex: ma-toolbox)",
170
+ visible=True,
171
+ info="Si 'new' : Nom de la nouvelle Toolbox (facultatif). Si 'existing' : Nom du Space à mettre à jour (obligatoire)."
172
+ )
173
+
174
+ # Petit helper pour changer le label/placeholder selon le mode (UX improvement)
175
+ def update_space_field(target):
176
+ if target == "new":
177
+ return gr.update(
178
+ label="Nom de la nouvelle Toolbox (Optionnel)",
179
+ placeholder="Laissez vide pour utiliser le nom du projet (ex: strawberry-counter)"
180
+ )
181
+ else:
182
+ return gr.update(
183
+ label="Nom du Space Existant (Obligatoire)",
184
+ placeholder="ex: username/my-toolbox"
185
+ )
186
+
187
+ space_target.change(fn=update_space_field, inputs=space_target, outputs=target_space_name)
188
+
189
+ btn_deploy = gr.Button("Déployer sur Spaces", variant="primary")
190
+ out_deploy = gr.JSON(label="Résultat du déploiement")
191
+
192
+ btn_deploy.click(
193
+ deploy_to_space_ui,
194
+ inputs=[draft_id_deploy, visibility, space_target, target_space_name],
195
+ outputs=out_deploy
196
+ )
197
+
198
+ # Câblage global des événements (une fois tous les composants définis)
199
+ # 1. Init -> Remplissage auto de l'onglet 2 (Logic) et copie de l'ID vers onglet 3 (Deploy)
200
+ btn_init.click(
201
+ init_and_propose_ui,
202
+ inputs=[project_name, project_desc, project_type, model_id, provider_id],
203
+ outputs=[out_init, draft_id_logic, python_code, inputs_dict, output_desc, requirements_box]
204
+ ).then(
205
+ fn=lambda x: x,
206
+ inputs=[draft_id_logic],
207
+ outputs=[draft_id_deploy]
208
+ )
209
+
210
+ with gr.Tab("4. Test & Playground (Smolagents)"):
211
+ gr.Markdown("Testez immédiatement votre serveur MCP déployé.")
212
+
213
+ with gr.Row():
214
+ mcp_url_input = gr.Textbox(
215
+ label="URL du Serveur MCP",
216
+ placeholder="ex: https://votre-user-votre-space.hf.space/gradio_api/mcp/sse",
217
+ scale=3
218
+ )
219
+ btn_reload = gr.Button("🔄 Charger les outils", scale=1)
220
+
221
+ status_msg = gr.Markdown("")
222
+ tool_table = gr.DataFrame(headers=["Tool name", "Description", "Params"], label="Outils détectés")
223
+
224
+ gr.Markdown("### 🤖 Discutez avec votre Agent MCP")
225
+ chatbot = gr.ChatInterface(
226
+ fn=chat_response_handler
227
+ )
228
+
229
+ btn_reload.click(
230
+ fn=reload_tools_handler,
231
+ inputs=[mcp_url_input],
232
+ outputs=[tool_table, status_msg]
233
+ )
234
+
235
+ # Exposition explicite des outils pour les agents MCP sans UI
236
+ # Cela permet à ChatGPT/Claude d'appeler ces fonctions directement
237
+ # Note: Les fonctions liées à l'UI sont déjà exposées, mais celles-ci sont plus propres pour une API.
238
+ # Gradio expose automatiquement les fonctions utilisées dans l'interface, mais on peut ajouter des endpoints API spécifiques.
239
+ # Cependant, avec mcp_server=True, Gradio expose TOUT ce qui est triggué.
240
+ # Pour être sûr que 'propose_implementation' est dispo, on l'ajoute via un composant invisible ou une API route si possible.
241
+ # Dans la version actuelle de Gradio MCP, seules les fonctions liées à des événements sont exposées.
242
+ # On va donc créer une "API Box" invisible pour exposer cet outil spécifique.
243
+
244
+ with gr.Accordion("API Tools (Invisible)", visible=False):
245
+ api_input_name = gr.Textbox()
246
+ api_input_desc = gr.Textbox()
247
+ api_output = gr.JSON()
248
+
249
+ btn_api_propose = gr.Button("Propose Implementation API")
250
+ btn_api_propose.click(
251
+ mcp_propose_implementation,
252
+ inputs=[api_input_name, api_input_desc],
253
+ outputs=[api_output],
254
+ api_name="propose_implementation" # Nom de l'outil pour le LLM
255
+ )
256
+
257
+ # --- Définition des Ressources et Prompts MCP ---
258
+
259
+ @gr.mcp.resource("list://drafts")
260
+ def list_active_drafts() -> str:
261
+ """Returns a list of currently active project drafts."""
262
+ # Note: In a real app, this would query the session manager
263
+ return "Active Drafts: [draft_id_1, draft_id_2]"
264
+
265
+ @gr.mcp.prompt()
266
+ def help_create_tool(topic: str = "general") -> str:
267
+ """
268
+ Provides a prompt template to help users create a new tool.
269
+ Args:
270
+ topic: The topic of the tool (e.g. 'data', 'fun', 'utility')
271
+ """
272
+ return f"I want to create a new MCP tool related to {topic}. Can you guide me through the initialization, logic definition, and deployment steps using the available tools?"
273
+
274
+ # Point d'entrée
275
+ if __name__ == "__main__":
276
+ # Lancement avec mcp_server=True pour exposer les outils aux LLMs
277
+ demo.launch(mcp_server=True)
src/mcp_server/tools.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+
3
+ from src.core.state.session_manager import SessionManager
4
+ from src.core.builder.code_generator import CodeGenerator
5
+ from src.core.deployer.huggingface import HFDeployer
6
+ from src.core.builder.proposal_generator import proposal_generator
7
+
8
+ # Initialisation des singletons
9
+ session_manager = SessionManager()
10
+ # Note: HFDeployer est instancié à la demande pour avoir le token le plus à jour ou géré par contexte si besoin
11
+ # Pour l'instant on l'instancie à chaque déploiement.
12
+
13
+ def init_project(project_name: str, description: str, type: str = "adhoc") -> Dict[str, Any]:
14
+ """
15
+ Crée un nouveau projet vide.
16
+ Args:
17
+ project_name: Nom technique (ex: strawberry-counter, ratp-api).
18
+ description: Description de l'outil, ou Spécification Technique complète (ex: contenu d'un Swagger/OpenAPI JSON).
19
+ type: 'adhoc' (code pur), 'api_wrapper' (REST).
20
+ Returns:
21
+ Un dictionnaire contenant le 'draft_id' nécessaire pour la suite.
22
+ """
23
+ draft = session_manager.create_draft(project_name, description, type)
24
+ return {
25
+ "draft_id": draft.draft_id,
26
+ "config": {
27
+ "name": draft.name,
28
+ "description": draft.description,
29
+ "files": list(draft.code_files.keys())
30
+ },
31
+ "message": f"Projet '{project_name}' initialisé. Draft ID: {draft.draft_id}"
32
+ }
33
+
34
+ def propose_implementation(project_name: str, description: str) -> Dict[str, Any]:
35
+ """
36
+ Utilise l'IA interne pour proposer une implémentation complète à partir d'une description ou d'un Swagger.
37
+ Args:
38
+ project_name: Le nom du projet.
39
+ description: La description ou le JSON Swagger/OpenAPI.
40
+ Returns:
41
+ Un dictionnaire contenant le code Python proposé, les inputs détectés et les requirements.
42
+ L'agent appelant peut ensuite valider ou modifier ce code avant d'appeler define_logic.
43
+ """
44
+ try:
45
+ proposal = proposal_generator.generate_from_description(project_name, description)
46
+ return {
47
+ "status": "success",
48
+ "proposal": proposal,
49
+ "message": "Implémentation proposée. Veuillez réviser 'python_code' et 'requirements' avant d'appeler define_logic."
50
+ }
51
+ except Exception as e:
52
+ return {"error": f"Erreur lors de la génération: {str(e)}"}
53
+
54
+ def define_logic(draft_id: str, python_code: str, inputs: Dict[str, str], output_desc: str, requirements: str = "") -> Dict[str, Any]:
55
+ """
56
+ Définit la logique interne de l'outil.
57
+ Génère à la fois le module modulaire et l'app maître.
58
+ """
59
+ draft = session_manager.get_draft(draft_id)
60
+ if not draft:
61
+ return {"error": f"Draft {draft_id} introuvable."}
62
+
63
+ # 1. Génération du module de l'outil (ex: tools/strawberry_counter.py)
64
+ # On utilise le nom du projet comme nom de fichier (nettoyé)
65
+ tool_filename = draft.name.replace("-", "_").lower()
66
+ tool_module_code = CodeGenerator.generate_tool_module(python_code, inputs, output_desc, draft.name)
67
+
68
+ # 2. Génération de l'application maître (app.py)
69
+ master_app_code = CodeGenerator.generate_master_app()
70
+
71
+ # Sauvegarde dans le draft
72
+ # On place l'outil dans un sous-dossier 'tools'
73
+ session_manager.update_code(draft_id, f"tools/{tool_filename}.py", tool_module_code)
74
+ session_manager.update_code(draft_id, "tools/__init__.py", "") # Package marker
75
+ session_manager.update_code(draft_id, "app.py", master_app_code)
76
+
77
+ # Mise à jour des requirements
78
+ current_reqs = draft.code_files.get("requirements.txt", "")
79
+ new_reqs = current_reqs
80
+
81
+ # Ajout de gradio si manquant
82
+ if "gradio" not in new_reqs:
83
+ new_reqs += "\ngradio"
84
+
85
+ # Ajout des requirements spécifiques demandés par le LLM
86
+ if requirements:
87
+ # requirements peut être une liste ou une chaine (si via UI Textbox)
88
+ if isinstance(requirements, list):
89
+ req_list = requirements
90
+ else:
91
+ req_list = [r.strip() for r in requirements.split(",") if r.strip()]
92
+
93
+ for req in req_list:
94
+ if req and req not in new_reqs:
95
+ new_reqs += f"\n{req}"
96
+
97
+ draft.code_files["requirements.txt"] = new_reqs.strip()
98
+
99
+ return {
100
+ "status": "success",
101
+ "message": f"Logique générée pour '{draft.name}'. Prêt à déployer.",
102
+ "preview": tool_module_code[:200] + "..."
103
+ }
104
+
105
+ def deploy_to_space(draft_id: str, visibility: str = "public", space_target: str = "new", target_space_name: str = "") -> Dict[str, Any]:
106
+ """
107
+ Déploie le projet sur Hugging Face Spaces.
108
+ Args:
109
+ draft_id: ID du draft.
110
+ visibility: 'public' ou 'private'.
111
+ space_target: 'new' (créer une nouvelle toolbox) ou 'existing' (ajouter à une toolbox existante).
112
+ target_space_name: Nom forcé du space cible (optionnel pour 'new', obligatoire pour 'existing').
113
+ """
114
+ draft = session_manager.get_draft(draft_id)
115
+ if not draft:
116
+ return {"error": f"Draft {draft_id} introuvable."}
117
+
118
+ deployer = HFDeployer()
119
+
120
+ # Détermination du nom du Space cible
121
+ # Si target_space_name est vide, on utilise le nom du projet
122
+ final_space_name = target_space_name if target_space_name else draft.name
123
+
124
+ # Filtrage des fichiers à déployer
125
+ files_to_deploy = draft.code_files.copy()
126
+
127
+ # Si on ajoute à un space existant, on n'écrase pas le loader principal (app.py)
128
+ if space_target == "existing":
129
+ if "app.py" in files_to_deploy:
130
+ del files_to_deploy["app.py"]
131
+ # On garde requirements.txt ? Idéalement il faudrait merger.
132
+ # Pour simplifier, on l'enlève pour éviter d'écraser des déps existantes.
133
+ if "requirements.txt" in files_to_deploy:
134
+ del files_to_deploy["requirements.txt"]
135
+
136
+ try:
137
+ url = deployer.deploy_space(
138
+ space_name=final_space_name,
139
+ files=files_to_deploy,
140
+ sdk="gradio",
141
+ private=(visibility == "private")
142
+ )
143
+
144
+ mode_msg = "ajouté à la toolbox" if space_target == "existing" else "déployé (nouveau space)"
145
+
146
+ # URL standard MCP pour Gradio (sans /sse explicite, compatible Claude Desktop)
147
+ mcp_endpoint = url.rstrip("/") + "/gradio_api/mcp/"
148
+ claude_config = f"""
149
+ {{
150
+ "mcpServers": {{
151
+ "{draft.name}": {{
152
+ "url": "{mcp_endpoint}"
153
+ }}
154
+ }}
155
+ }}
156
+ """
157
+
158
+ return {
159
+ "status": "success",
160
+ "url": url,
161
+ "instructions": f"Outil '{draft.name}' {mode_msg} !",
162
+ "claude_config": claude_config
163
+ }
164
+ except Exception as e:
165
+ return {"error": f"Erreur de déploiement: {str(e)}"}
tests/test_deploy.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from dotenv import load_dotenv
4
+
5
+ # Ajout du dossier src au path pour pouvoir importer les modules
6
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
7
+
8
+ from core.deployer.huggingface import HFDeployer
9
+
10
+ def test_deployment():
11
+ # Charger les variables d'environnement (pour HF_TOKEN)
12
+ load_dotenv()
13
+
14
+ token = os.environ.get("HF_TOKEN")
15
+ if not token:
16
+ print("❌ Erreur: HF_TOKEN manquant dans les variables d'environnement.")
17
+ print("Veuillez créer un fichier .env avec HF_TOKEN=votre_token_write")
18
+ return
19
+
20
+ print("🧪 Démarrage du test de déploiement...")
21
+
22
+ deployer = HFDeployer(token=token)
23
+
24
+ # Nom unique pour le test (avec timestamp pour éviter les conflits si possible,
25
+ # mais pour simplifier on utilise un nom fixe 'test-meta-mcp-hello' que l'utilisateur pourra supprimer)
26
+ space_name = "test-meta-mcp-hello"
27
+
28
+ # Code de l'application "Hello World"
29
+ app_code = """
30
+ import gradio as gr
31
+
32
+ def greet(name):
33
+ return "Hello " + name + " from Meta-MCP!"
34
+
35
+ iface = gr.Interface(fn=greet, inputs="text", outputs="text")
36
+ iface.launch()
37
+ """
38
+
39
+ files = {
40
+ "app.py": app_code,
41
+ "requirements.txt": "gradio"
42
+ }
43
+
44
+ try:
45
+ url = deployer.deploy_space(
46
+ space_name=space_name,
47
+ files=files,
48
+ sdk="gradio",
49
+ private=False
50
+ )
51
+ print(f"\n✅ Test réussi ! Space déployé sur : {url}")
52
+ print("⚠️ N'oubliez pas de supprimer ce Space manuellement si vous ne souhaitez pas le conserver.")
53
+
54
+ except Exception as e:
55
+ print(f"\n❌ Échec du test : {str(e)}")
56
+
57
+ if __name__ == "__main__":
58
+ test_deployment()