77
88from mcp .server .fastmcp import FastMCP
99from mcp .server .fastmcp import Image
10+ from mcp .types import CallToolResult
11+ from mcp .types import TextContent
1012import optuna
1113import optuna_dashboard
1214import plotly
@@ -110,21 +112,23 @@ def create_study(
110112 return StudyResponse (study_name = study_name )
111113
112114 @mcp .tool (structured_output = True )
113- def get_all_study_names () -> list [StudyResponse ] | str :
115+ def get_all_study_names () -> list [StudyResponse ] | CallToolResult :
114116 """Get all study names from the storage."""
115117 storage : str | optuna .storages .BaseStorage | None = None
116118 if mcp .study is not None :
117119 storage = mcp .study ._storage
118120 elif mcp .storage is not None :
119121 storage = mcp .storage
120122 else :
121- return "No storage specified."
123+ return CallToolResult (
124+ content = [TextContent (type = "text" , text = "No storage specified." )], isError = True
125+ )
122126
123127 study_names = optuna .get_all_study_names (storage )
124128 return [StudyResponse (study_name = name ) for name in study_names ]
125129
126130 @mcp .tool (structured_output = True )
127- def ask (search_space : dict ) -> TrialResponse | str :
131+ def ask (search_space : dict ) -> TrialResponse | CallToolResult :
128132 """Suggest new parameters using Optuna
129133
130134 search_space must be a string that can be evaluated to a dictionary to specify Optuna's distributions.
@@ -138,10 +142,19 @@ def ask(search_space: dict) -> TrialResponse | str:
138142 for name , dist in search_space .items ()
139143 }
140144 except Exception as e :
141- return f"Error: { e } "
145+ return CallToolResult (
146+ content = [TextContent (type = "text" , text = f"Error: { e } " )], isError = True
147+ )
142148
143149 if mcp .study is None :
144- raise ValueError ("No study has been created. Please create a study first." )
150+ return CallToolResult (
151+ content = [
152+ TextContent (
153+ type = "text" , text = "No study has been created. Please create a study first."
154+ )
155+ ],
156+ isError = True ,
157+ )
145158
146159 trial = mcp .study .ask (fixed_distributions = distributions )
147160
@@ -151,10 +164,17 @@ def ask(search_space: dict) -> TrialResponse | str:
151164 )
152165
153166 @mcp .tool (structured_output = True )
154- def tell (trial_number : int , values : float | list [float ]) -> TrialResponse :
167+ def tell (trial_number : int , values : float | list [float ]) -> TrialResponse | CallToolResult :
155168 """Report the result of a trial"""
156169 if mcp .study is None :
157- raise ValueError ("No study has been created. Please create a study first." )
170+ return CallToolResult (
171+ content = [
172+ TextContent (
173+ type = "text" , text = "No study has been created. Please create a study first."
174+ )
175+ ],
176+ isError = True ,
177+ )
158178
159179 mcp .study .tell (
160180 trial = trial_number ,
@@ -170,7 +190,7 @@ def tell(trial_number: int, values: float | list[float]) -> TrialResponse:
170190 @mcp .tool (structured_output = True )
171191 def set_sampler (
172192 name : typing .Literal ["TPESampler" , "NSGAIISampler" , "RandomSampler" , "GPSampler" ],
173- ) -> StudyResponse :
193+ ) -> StudyResponse | CallToolResult :
174194 """Set the sampler for the study.
175195 The sampler must be one of the following:
176196 - TPESampler
@@ -184,18 +204,34 @@ def set_sampler(
184204 """
185205 sampler = getattr (optuna .samplers , name )()
186206 if mcp .study is None :
187- raise ValueError ("No study has been created. Please create a study first." )
207+ return CallToolResult (
208+ content = [
209+ TextContent (
210+ type = "text" , text = "No study has been created. Please create a study first."
211+ )
212+ ],
213+ isError = True ,
214+ )
188215 mcp .study .sampler = sampler
189216 return StudyResponse (
190217 study_name = mcp .study .study_name ,
191218 sampler_name = name ,
192219 )
193220
194221 @mcp .tool (structured_output = True )
195- def set_trial_user_attr (trial_number : int , key : str , value : typing .Any ) -> str :
222+ def set_trial_user_attr (
223+ trial_number : int , key : str , value : typing .Any
224+ ) -> str | CallToolResult :
196225 """Set user attributes for a trial"""
197226 if mcp .study is None :
198- raise ValueError ("No study has been created. Please create a study first." )
227+ return CallToolResult (
228+ content = [
229+ TextContent (
230+ type = "text" , text = "No study has been created. Please create a study first."
231+ )
232+ ],
233+ isError = True ,
234+ )
199235
200236 storage = mcp .study ._storage
201237 trial_id = storage .get_trial_id_from_study_id_trial_number (
@@ -205,10 +241,17 @@ def set_trial_user_attr(trial_number: int, key: str, value: typing.Any) -> str:
205241 return f"User attribute { key } set to { json .dumps (value )} for trial { trial_number } "
206242
207243 @mcp .tool (structured_output = True )
208- def get_trial_user_attrs (trial_number : int ) -> TrialResponse :
244+ def get_trial_user_attrs (trial_number : int ) -> TrialResponse | CallToolResult :
209245 """Get user attributes in a trial"""
210246 if mcp .study is None :
211- raise ValueError ("No study has been created. Please create a study first." )
247+ return CallToolResult (
248+ content = [
249+ TextContent (
250+ type = "text" , text = "No study has been created. Please create a study first."
251+ )
252+ ],
253+ isError = True ,
254+ )
212255 storage = mcp .study ._storage
213256 trial_id = storage .get_trial_id_from_study_id_trial_number (
214257 mcp .study ._study_id , trial_number
@@ -220,7 +263,7 @@ def get_trial_user_attrs(trial_number: int) -> TrialResponse:
220263 )
221264
222265 @mcp .tool (structured_output = True )
223- def set_metric_names (metric_names : list [str ]) -> str :
266+ def set_metric_names (metric_names : list [str ]) -> str | CallToolResult :
224267 """Set metric_names. metric_names are labels used to distinguish what each objective value is.
225268
226269 Args:
@@ -229,44 +272,79 @@ def set_metric_names(metric_names: list[str]) -> str:
229272 The length of metric_names list must be the same with the number of objectives.
230273 """
231274 if mcp .study is None :
232- raise ValueError ("No study has been created. Please create a study first." )
275+ return CallToolResult (
276+ content = [
277+ TextContent (
278+ type = "text" , text = "No study has been created. Please create a study first."
279+ )
280+ ],
281+ isError = True ,
282+ )
233283 mcp .study .set_metric_names (metric_names )
234284 return f"metric_names set to { json .dumps (metric_names )} "
235285
236286 @mcp .tool (structured_output = True )
237- def get_metric_names () -> str :
287+ def get_metric_names () -> str | CallToolResult :
238288 """Get metric_names"""
239289 if mcp .study is None :
240- raise ValueError ("No study has been created. Please create a study first." )
290+ return CallToolResult (
291+ content = [
292+ TextContent (
293+ type = "text" , text = "No study has been created. Please create a study first."
294+ )
295+ ],
296+ isError = True ,
297+ )
241298 return f"Metric names: { json .dumps (mcp .study .metric_names )} "
242299
243300 @mcp .tool (structured_output = True )
244- def get_directions () -> StudyResponse :
301+ def get_directions () -> StudyResponse | CallToolResult :
245302 """Get the directions of the study."""
246303 if mcp .study is None :
247- raise ValueError ("No study has been created. Please create a study first." )
304+ return CallToolResult (
305+ content = [
306+ TextContent (
307+ type = "text" , text = "No study has been created. Please create a study first."
308+ )
309+ ],
310+ isError = True ,
311+ )
248312 directions = [d .name .lower () for d in mcp .study .directions ]
249313 return StudyResponse (
250314 study_name = mcp .study .study_name ,
251315 directions = directions ,
252316 )
253317
254318 @mcp .tool (structured_output = True )
255- def get_trials () -> str :
319+ def get_trials () -> str | CallToolResult :
256320 """Get all trials in a CSV format"""
257321 if mcp .study is None :
258- raise ValueError ("No study has been created. Please create a study first." )
322+ return CallToolResult (
323+ content = [
324+ TextContent (
325+ type = "text" , text = "No study has been created. Please create a study first."
326+ )
327+ ],
328+ isError = True ,
329+ )
259330 csv_string = mcp .study .trials_dataframe ().to_csv ()
260331 return f"Trials: \n { csv_string } "
261332
262333 @mcp .tool (structured_output = True )
263- def best_trial () -> TrialResponse :
334+ def best_trial () -> TrialResponse | CallToolResult :
264335 """Get the best trial
265336
266337 This feature can only be used for single-objective optimization. If your study is multi-objective, use best_trials instead.
267338 """
268339 if mcp .study is None :
269- raise ValueError ("No study has been created. Please create a study first." )
340+ return CallToolResult (
341+ content = [
342+ TextContent (
343+ type = "text" , text = "No study has been created. Please create a study first."
344+ )
345+ ],
346+ isError = True ,
347+ )
270348
271349 trial = mcp .study .best_trial
272350 return TrialResponse (
@@ -278,10 +356,17 @@ def best_trial() -> TrialResponse:
278356 )
279357
280358 @mcp .tool (structured_output = True )
281- def best_trials () -> list [TrialResponse ]:
359+ def best_trials () -> list [TrialResponse ] | CallToolResult :
282360 """Return trials located at the Pareto front in the study."""
283361 if mcp .study is None :
284- raise ValueError ("No study has been created. Please create a study first." )
362+ return CallToolResult (
363+ content = [
364+ TextContent (
365+ type = "text" , text = "No study has been created. Please create a study first."
366+ )
367+ ],
368+ isError = True ,
369+ )
285370 return [
286371 TrialResponse (
287372 trial_number = trial .number ,
@@ -308,19 +393,33 @@ def _create_trial(trial: TrialToAdd) -> optuna.trial.FrozenTrial:
308393 )
309394
310395 @mcp .tool (structured_output = True )
311- def add_trial (trial : TrialToAdd ) -> str :
396+ def add_trial (trial : TrialToAdd ) -> str | CallToolResult :
312397 """Add a trial to the study."""
313398 if mcp .study is None :
314- raise ValueError ("No study has been created. Please create a study first." )
399+ return CallToolResult (
400+ content = [
401+ TextContent (
402+ type = "text" , text = "No study has been created. Please create a study first."
403+ )
404+ ],
405+ isError = True ,
406+ )
315407 mcp .study .add_trial (_create_trial (trial ))
316408 return "Trial was added."
317409
318410 @mcp .tool (structured_output = True )
319- def add_trials (trials : list [TrialToAdd ]) -> str :
411+ def add_trials (trials : list [TrialToAdd ]) -> str | CallToolResult :
320412 """Add multiple trials to the study."""
321413 frozen_trials = [_create_trial (trial ) for trial in trials ]
322414 if mcp .study is None :
323- raise ValueError ("No study has been created. Please create a study first." )
415+ return CallToolResult (
416+ content = [
417+ TextContent (
418+ type = "text" , text = "No study has been created. Please create a study first."
419+ )
420+ ],
421+ isError = True ,
422+ )
324423 mcp .study .add_trials (frozen_trials )
325424 return f"{ len (trials )} trials were added."
326425
@@ -541,7 +640,7 @@ def plot_rank(
541640 return Image (data = plotly .io .to_image (fig ), format = "png" )
542641
543642 @mcp .tool (structured_output = True )
544- def launch_optuna_dashboard (port : int = 58080 ) -> str :
643+ def launch_optuna_dashboard (port : int = 58080 ) -> str | CallToolResult :
545644 """Launch the Optuna dashboard"""
546645 storage : str | optuna .storages .BaseStorage | None = None
547646 if mcp .dashboard_thread_port is not None :
@@ -552,9 +651,14 @@ def launch_optuna_dashboard(port: int = 58080) -> str:
552651 elif mcp .storage is not None :
553652 storage = mcp .storage
554653 else :
555- raise ValueError (
556- "No study has been created or no storage URL has been provided."
557- "Please create a study or provide a storage URL directly."
654+ return CallToolResult (
655+ content = [
656+ TextContent (
657+ type = "text" ,
658+ text = "No study has been created or no storage URL has been provided. Please create a study or provide a storage URL directly." ,
659+ )
660+ ],
661+ isError = True ,
558662 )
559663
560664 def runner (storage : optuna .storages .BaseStorage | str , port : int ) -> None :
0 commit comments