Ali Hmaou commited on
Commit
e5bc2ce
·
1 Parent(s): 8523d41
Files changed (3) hide show
  1. app.py +9 -274
  2. src/mcp_server/server.py +49 -16
  3. src/mcp_server/tools.py +35 -2
app.py CHANGED
@@ -1,280 +1,15 @@
1
- import gradio as gr
2
  import os
3
  import sys
4
 
5
- # Plus besoin du sys.path hack car on est à la racine
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
- api_name=False # Hide internal tool
234
- )
235
-
236
- # Exposition explicite des outils pour les agents MCP sans UI
237
- # Cela permet à ChatGPT/Claude d'appeler ces fonctions directement
238
- # Note: Les fonctions liées à l'UI sont déjà exposées, mais celles-ci sont plus propres pour une API.
239
- # Gradio expose automatiquement les fonctions utilisées dans l'interface, mais on peut ajouter des endpoints API spécifiques.
240
- # Cependant, avec mcp_server=True, Gradio expose TOUT ce qui est triggué.
241
- # Pour être sûr que 'propose_implementation' est dispo, on l'ajoute via un composant invisible ou une API route si possible.
242
- # Dans la version actuelle de Gradio MCP, seules les fonctions liées à des événements sont exposées.
243
- # On va donc créer une "API Box" invisible pour exposer cet outil spécifique.
244
-
245
- with gr.Accordion("API Tools (Invisible)", visible=False):
246
- api_input_name = gr.Textbox()
247
- api_input_desc = gr.Textbox()
248
- api_output = gr.JSON()
249
-
250
- btn_api_propose = gr.Button("Propose Implementation API")
251
- btn_api_propose.click(
252
- mcp_propose_implementation,
253
- inputs=[api_input_name, api_input_desc],
254
- outputs=[api_output],
255
- api_name="propose_implementation" # Nom de l'outil pour le LLM
256
- )
257
-
258
- # --- Définition des Ressources et Prompts MCP ---
259
-
260
- # On active les décorateurs s'ils sont dispos
261
- if hasattr(gr, "mcp"):
262
- @gr.mcp.resource("list://drafts")
263
- def list_active_drafts() -> str:
264
- """Returns a list of currently active project drafts."""
265
- # Note: In a real app, this would query the session manager
266
- return "Active Drafts: [draft_id_1, draft_id_2]"
267
-
268
- @gr.mcp.prompt()
269
- def help_create_tool(topic: str = "general") -> str:
270
- """
271
- Provides a prompt template to help users create a new tool.
272
- Args:
273
- topic: The topic of the tool (e.g. 'data', 'fun', 'utility')
274
- """
275
- 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?"
276
-
277
- # Point d'entrée
278
  if __name__ == "__main__":
279
- # Lancement avec mcp_server=True pour exposer les outils aux LLMs
280
- demo.launch(show_error=True, mcp_server=True)
 
 
 
 
1
  import os
2
  import sys
3
 
4
+ # Ajoute le dossier courant au path pour pouvoir importer src
5
+ sys.path.append(os.path.dirname(__file__))
6
 
7
+ # Importe l'interface Gradio depuis le serveur
8
+ # Note: Assurez-vous que src/mcp_server/server.py définit bien une variable 'demo' (gr.Blocks)
9
+ from src.mcp_server.server import demo
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  if __name__ == "__main__":
12
+ # Lancement du serveur compatible Hugging Face Spaces
13
+ # mcp_server=True active les endpoints MCP
14
+ # show_error=True permet de voir les erreurs Python dans l'interface (utile pour le débug)
15
+ demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True, show_error=True)
src/mcp_server/server.py CHANGED
@@ -31,9 +31,12 @@ def step_1_initialisation_and_proposal(project_name, description, type, model_id
31
  draft_id = init_result.get("draft_id", "")
32
 
33
  # 2. Génération de la proposition par LLM
 
34
  print(f"🤖 Génération de la proposition pour : {project_name} (Model: {model_id}, Provider: {provider_id})...")
35
  proposal = proposal_generator.generate_from_description(project_name, description, model=model_id, provider=provider_id)
36
 
 
 
37
  # 3. Retourne les données pour mettre à jour l'UI
38
  # Gère le cas où 'requirements' n'est pas renvoyé par le LLM
39
  reqs = proposal.get("requirements", [])
@@ -68,6 +71,12 @@ def step_2_logic_definition(draft_id, python_code, inputs, output_desc, requirem
68
  print(f"DEBUG [step_2_logic_definition]: inputs type={type(inputs)}")
69
 
70
  result = tools.define_logic(draft_id, python_code, inputs, output_desc, requirements)
 
 
 
 
 
 
71
  return result
72
 
73
  def step_3_deployment(draft_id):
@@ -79,8 +88,15 @@ def step_3_deployment(draft_id):
79
  Args:
80
  draft_id: The unique ID of the project draft (from Step 1).
81
  """
 
82
  # Simplification: Toujours public, toujours new (écrase/crée), nom du space = nom du projet
83
  result = tools.deploy_to_space(draft_id, visibility="public", space_target="new", target_space_name=None)
 
 
 
 
 
 
84
  return result
85
 
86
  # Récupération des handlers du playground
@@ -191,6 +207,7 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
191
  if user: os.environ["HF_USER"] = user
192
  if space: os.environ["DEFAULT_SPACE"] = space
193
  if token: os.environ["HF_TOKEN"] = token
 
194
  return f"Configuration saved! User: {user}, Default Space: {space}"
195
 
196
  config_status = gr.Markdown("")
@@ -320,8 +337,6 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
320
  if "/" in path:
321
  user, space = path.split("/", 1)
322
  # Format direct url : https://user-space.hf.space
323
- # Note: le nom du space dans l'url doit être en minuscules et les _ deviennent - parfois, mais HF gère la redirection souvent.
324
- # Pour être sûr, on garde la structure simple.
325
  direct_url = f"https://{user}-{space}.hf.space/gradio_api/mcp/"
326
  return direct_url
327
  except:
@@ -330,18 +345,6 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
330
  # Fallback si parsing échoue
331
  return hf_url
332
 
333
- btn_deploy.click(
334
- step_3_deployment,
335
- inputs=[draft_id_deploy],
336
- outputs=out_deploy,
337
- api_name="step_3_deployment"
338
- ).then(
339
- fn=auto_fill_playground,
340
- inputs=[out_deploy],
341
- outputs=[mcp_url_input],
342
- api_name=False
343
- )
344
-
345
  # Câblage global des événements (une fois tous les composants définis)
346
  # 1. Init -> Remplissage auto de l'onglet 2 (Logic) et copie de l'ID vers onglet 3 (Deploy)
347
  btn_init.click(
@@ -359,7 +362,7 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
359
  with gr.Tab("4. Test & Playground (Smolagents)"):
360
  gr.Markdown("Testez immédiatement votre serveur MCP déployé.")
361
 
362
- with gr.Row():
363
  mcp_url_input = gr.Textbox(
364
  label="URL du Serveur MCP",
365
  placeholder="ex: https://votre-user-votre-space.hf.space/gradio_api/mcp/sse",
@@ -368,7 +371,24 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
368
  btn_reload = gr.Button("🔄 Charger les outils", scale=1)
369
 
370
  status_msg = gr.Markdown("")
371
- tool_table = gr.DataFrame(headers=["Tool name", "Description", "Params"], label="Outils détectés")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  gr.Markdown("### 🤖 Discutez avec votre Agent MCP")
374
  chatbot = gr.ChatInterface(
@@ -382,6 +402,19 @@ with gr.Blocks(title="Meta-MCP Fractal") as demo:
382
  api_name=False # Hide internal tool
383
  )
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  # Exposition explicite des outils pour les agents MCP sans UI
386
  # Cela permet à ChatGPT/Claude d'appeler ces fonctions directement
387
  # Note: Les fonctions liées à l'UI sont déjà exposées, mais celles-ci sont plus propres pour une API.
 
31
  draft_id = init_result.get("draft_id", "")
32
 
33
  # 2. Génération de la proposition par LLM
34
+ gr.Info("Génération du code par l'IA en cours...")
35
  print(f"🤖 Génération de la proposition pour : {project_name} (Model: {model_id}, Provider: {provider_id})...")
36
  proposal = proposal_generator.generate_from_description(project_name, description, model=model_id, provider=provider_id)
37
 
38
+ gr.Info("Proposition générée ! Veuillez valider dans l'onglet suivant.")
39
+
40
  # 3. Retourne les données pour mettre à jour l'UI
41
  # Gère le cas où 'requirements' n'est pas renvoyé par le LLM
42
  reqs = proposal.get("requirements", [])
 
71
  print(f"DEBUG [step_2_logic_definition]: inputs type={type(inputs)}")
72
 
73
  result = tools.define_logic(draft_id, python_code, inputs, output_desc, requirements)
74
+
75
+ if "error" not in result:
76
+ gr.Info("Code validé et enregistré ! Prêt à déployer.")
77
+ else:
78
+ gr.Info(f"Erreur : {result['error']}")
79
+
80
  return result
81
 
82
  def step_3_deployment(draft_id):
 
88
  Args:
89
  draft_id: The unique ID of the project draft (from Step 1).
90
  """
91
+ gr.Info("Déploiement en cours... Cela peut prendre quelques minutes.")
92
  # Simplification: Toujours public, toujours new (écrase/crée), nom du space = nom du projet
93
  result = tools.deploy_to_space(draft_id, visibility="public", space_target="new", target_space_name=None)
94
+
95
+ if "error" not in result:
96
+ gr.Info(f"Déploiement réussi ! URL : {result.get('url')}")
97
+ else:
98
+ gr.Info(f"Échec du déploiement : {result.get('error')}")
99
+
100
  return result
101
 
102
  # Récupération des handlers du playground
 
207
  if user: os.environ["HF_USER"] = user
208
  if space: os.environ["DEFAULT_SPACE"] = space
209
  if token: os.environ["HF_TOKEN"] = token
210
+ gr.Info("Configuration sauvegardée !")
211
  return f"Configuration saved! User: {user}, Default Space: {space}"
212
 
213
  config_status = gr.Markdown("")
 
337
  if "/" in path:
338
  user, space = path.split("/", 1)
339
  # Format direct url : https://user-space.hf.space
 
 
340
  direct_url = f"https://{user}-{space}.hf.space/gradio_api/mcp/"
341
  return direct_url
342
  except:
 
345
  # Fallback si parsing échoue
346
  return hf_url
347
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  # Câblage global des événements (une fois tous les composants définis)
349
  # 1. Init -> Remplissage auto de l'onglet 2 (Logic) et copie de l'ID vers onglet 3 (Deploy)
350
  btn_init.click(
 
362
  with gr.Tab("4. Test & Playground (Smolagents)"):
363
  gr.Markdown("Testez immédiatement votre serveur MCP déployé.")
364
 
365
+ with gr.Column():
366
  mcp_url_input = gr.Textbox(
367
  label="URL du Serveur MCP",
368
  placeholder="ex: https://votre-user-votre-space.hf.space/gradio_api/mcp/sse",
 
371
  btn_reload = gr.Button("🔄 Charger les outils", scale=1)
372
 
373
  status_msg = gr.Markdown("")
374
+
375
+ # Tableau adapté pour l'affichage des outils (wrap=True)
376
+ tool_table = gr.DataFrame(
377
+ headers=["Tool name", "Description", "Params"],
378
+ label="Outils détectés",
379
+ wrap=True,
380
+ interactive=False
381
+ )
382
+
383
+ gr.Markdown("""
384
+ ### ⚙️ Configuration Smolagents
385
+ Pour utiliser cet outil avec smolagents dans votre code :
386
+ ```python
387
+ from smolagents import MCPClient
388
+ # Mode HTTP direct (recommandé)
389
+ client = MCPClient(url="URL_DU_SERVEUR", structured_output=False)
390
+ ```
391
+ """)
392
 
393
  gr.Markdown("### 🤖 Discutez avec votre Agent MCP")
394
  chatbot = gr.ChatInterface(
 
402
  api_name=False # Hide internal tool
403
  )
404
 
405
+ # Câblage différé du déploiement (pour avoir accès à mcp_url_input défini dans le Tab 4)
406
+ btn_deploy.click(
407
+ step_3_deployment,
408
+ inputs=[draft_id_deploy],
409
+ outputs=out_deploy,
410
+ api_name="step_3_deployment"
411
+ ).then(
412
+ fn=auto_fill_playground,
413
+ inputs=[out_deploy],
414
+ outputs=[mcp_url_input],
415
+ api_name=False
416
+ )
417
+
418
  # Exposition explicite des outils pour les agents MCP sans UI
419
  # Cela permet à ChatGPT/Claude d'appeler ces fonctions directement
420
  # Note: Les fonctions liées à l'UI sont déjà exposées, mais celles-ci sont plus propres pour une API.
src/mcp_server/tools.py CHANGED
@@ -1,6 +1,8 @@
1
  from typing import Dict, Any, Union
2
  import json
3
  import os
 
 
4
 
5
  from src.core.state.session_manager import SessionManager
6
  from src.core.builder.code_generator import CodeGenerator
@@ -157,8 +159,40 @@ def deploy_to_space(draft_id: str, visibility: str = "public", space_target: str
157
  if space_target == "existing":
158
  if "app.py" in files_to_deploy:
159
  del files_to_deploy["app.py"]
 
 
160
  if "requirements.txt" in files_to_deploy:
161
- del files_to_deploy["requirements.txt"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  try:
164
  url = deployer.deploy_space(
@@ -187,7 +221,6 @@ def deploy_to_space(draft_id: str, visibility: str = "public", space_target: str
187
  "{server_name}": {{
188
  "command": "npx",
189
  "args": [
190
- "-y",
191
  "mcp-remote",
192
  "{mcp_endpoint}",
193
  "--transport",
 
1
  from typing import Dict, Any, Union
2
  import json
3
  import os
4
+ from huggingface_hub import hf_hub_download
5
+ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
6
 
7
  from src.core.state.session_manager import SessionManager
8
  from src.core.builder.code_generator import CodeGenerator
 
159
  if space_target == "existing":
160
  if "app.py" in files_to_deploy:
161
  del files_to_deploy["app.py"]
162
+
163
+ # Fusion intelligente des requirements
164
  if "requirements.txt" in files_to_deploy:
165
+ new_reqs = set(files_to_deploy["requirements.txt"].splitlines())
166
+
167
+ try:
168
+ print(f"DEBUG: Tentative de récupération des requirements existants sur {final_space_name}...")
169
+ # Téléchargement du requirements.txt existant
170
+ cached_path = hf_hub_download(
171
+ repo_id=final_space_name,
172
+ filename="requirements.txt",
173
+ repo_type="space",
174
+ token=deployer.token
175
+ )
176
+ with open(cached_path, 'r') as f:
177
+ existing_reqs = set(f.read().splitlines())
178
+
179
+ # Fusion (Union)
180
+ merged_reqs = existing_reqs.union(new_reqs)
181
+ # Nettoyage (lignes vides)
182
+ merged_content = "\n".join(sorted([r for r in merged_reqs if r.strip()]))
183
+
184
+ files_to_deploy["requirements.txt"] = merged_content
185
+ print(f"DEBUG: Requirements fusionnés avec succès ({len(existing_reqs)} existants + nouveaux).")
186
+
187
+ except (EntryNotFoundError, RepositoryNotFoundError):
188
+ print("DEBUG: Pas de requirements.txt existant ou repo introuvable, utilisation des nouveaux uniquement.")
189
+ # Le fichier n'existe pas encore, on garde celui du draft
190
+ pass
191
+ except Exception as e:
192
+ print(f"DEBUG: Erreur lors de la fusion des requirements: {e}")
193
+ # En cas d'erreur, on garde les nouveaux pour ne pas bloquer,
194
+ # mais on pourrait aussi choisir d'échouer. Ici on force l'ajout.
195
+ pass
196
 
197
  try:
198
  url = deployer.deploy_space(
 
221
  "{server_name}": {{
222
  "command": "npx",
223
  "args": [
 
224
  "mcp-remote",
225
  "{mcp_endpoint}",
226
  "--transport",