guohanghui commited on
Commit
d1f7934
·
verified ·
1 Parent(s): 3123d2d

Update climlab/mcp_output/mcp_plugin/mcp_service.py

Browse files
climlab/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,400 +1,739 @@
 
 
 
 
 
 
 
 
 
1
  from fastmcp import FastMCP
2
 
3
- # 创建 FastMCP 服务应用
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  mcp = FastMCP("climlab_service")
5
 
6
- @mcp.tool(name="get_version", description="获取 climlab 的版本信息")
7
- def get_version() -> dict:
 
 
 
8
  """
9
- 获取 climlab 的版本信息。
10
 
11
- Returns:
12
- - dict: 包含版本号的字典。
 
 
13
  """
14
  try:
15
- from importlib import metadata
16
- version = metadata.version("climlab")
17
- return {"success": True, "version": version}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  except Exception as e:
19
- return {"success": False, "error": str(e)}
 
20
 
21
- @mcp.tool(name="create_ebm_model", description="创建一个 1D 能量平衡模型 (EBM)")
22
- def create_ebm_model() -> dict:
23
  """
24
- 创建一个 1D 能量平衡模型。
25
 
26
- Returns:
27
- - dict: 包含模型信息的字典。
 
 
 
28
  """
29
  try:
30
- from climlab import EBM
31
- model = EBM()
32
- return {
33
- "success": True,
34
- "model": str(model),
35
- "state": model.state,
36
- "diagnostics": list(model.diagnostics.keys())
 
 
 
37
  }
 
38
  except Exception as e:
39
- return {"success": False, "error": str(e)}
40
 
41
- @mcp.tool(name="integrate_model", description="运行模型指定的时间步长")
42
- def integrate_model(model, years: float) -> dict:
43
- """
44
- 运行模型指定的时间步长。
45
 
46
- Parameters:
47
- - model: climlab 模型实例。
48
- - years: 运行的年数。
 
 
 
 
 
49
 
50
- Returns:
51
- - dict: 包含运行状态和结果的字典。
 
 
 
 
 
 
 
 
 
52
  """
53
  try:
54
- model.integrate_years(years)
55
- return {
56
- "success": True,
57
- "state": model.state,
58
- "diagnostics": model.diagnostics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
 
60
  except Exception as e:
61
- return {"success": False, "error": str(e)}
62
 
63
- @mcp.tool(name="list_available_models", description="列出 climlab 中的所有可用模型")
64
- def list_available_models() -> dict:
 
 
 
 
65
  """
66
- 列出 climlab 中的所有可用模型。
67
 
68
- Returns:
69
- - dict: 包含模型名称的字典。
 
 
 
 
 
 
 
 
 
70
  """
71
  try:
72
- models = ["EBM", "RCM", "GreyRadiationModel", "BandRCModel"]
73
- return {"success": True, "models": models}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
75
- return {"success": False, "error": str(e)}
76
 
77
- @mcp.tool(name="create_model", description="根据名称创建 climlab 模型")
78
- def create_model(model_name: str) -> dict:
79
- """
80
- 根据名称创建 climlab 模型。
81
 
82
- Parameters:
83
- - model_name: 模型名称 (例如 'EBM', 'RCM')。
 
 
 
84
 
85
- Returns:
86
- - dict: 包含模型信息的字典。
 
 
87
  """
88
  try:
89
- from climlab import EBM, RadiativeConvectiveModel, GreyRadiationModel, BandRCModel
90
- models = {
91
- "EBM": EBM,
92
- "RCM": RadiativeConvectiveModel,
93
- "GreyRadiationModel": GreyRadiationModel,
94
- "BandRCModel": BandRCModel
95
- }
96
- if model_name not in models:
97
- return {"success": False, "error": f"Model '{model_name}' not found."}
98
-
99
- model = models[model_name]()
100
- return {
101
- "success": True,
102
- "model": str(model),
103
- "state": model.state,
104
- "diagnostics": list(model.diagnostics.keys())
105
  }
 
106
  except Exception as e:
107
- return {"success": False, "error": str(e)}
108
 
109
- @mcp.tool(name="create_convective_adjustment", description="创建一个对流调整模型")
110
- def create_convective_adjustment(adj_lapse_rate: float) -> dict:
111
- """
112
- 创建一个对流调整模型。
113
 
114
- Parameters:
115
- - adj_lapse_rate: 调整的递减率 (单位: K/km)。
116
 
117
- Returns:
118
- - dict: 包含模型信息的字典。
 
 
 
 
 
 
 
 
 
 
 
 
119
  """
120
  try:
121
- from climlab.convection.convadj import ConvectiveAdjustment
122
- model = ConvectiveAdjustment(adj_lapse_rate=adj_lapse_rate)
123
- return {
124
- "success": True,
125
- "model": str(model),
126
- "state": model.state
 
 
 
127
  }
 
128
  except Exception as e:
129
- return {"success": False, "error": str(e)}
 
130
 
131
- @mcp.tool(name="create_emanuel_convection", description="创建一个Emanuel对流模型")
132
- def create_emanuel_convection() -> dict:
 
 
133
  """
134
- 创建一个Emanuel对流模型。
135
 
136
- Returns:
137
- - dict: 包含模型信息的字典。
 
 
 
 
138
  """
139
  try:
140
- from climlab.convection.emanuel_convection import EmanuelConvection
141
- model = EmanuelConvection()
142
- return {
143
- "success": True,
144
- "model": str(model),
145
- "state": model.state
 
 
 
 
 
146
  }
 
147
  except Exception as e:
148
- return {"success": False, "error": str(e)}
149
 
150
- @mcp.tool(name="create_grey_gas_model", description="创建一个灰气辐射模型")
151
- def create_grey_gas_model(absorptivity: float) -> dict:
152
- """
153
- 创建一个灰气辐射模型。
154
 
155
- Parameters:
156
- - absorptivity: 吸收率。
 
 
 
 
157
 
158
- Returns:
159
- - dict: 包含模型信息的字典。
 
 
 
 
160
  """
161
  try:
162
- from climlab.radiation.greygas import GreyGas
163
- model = GreyGas(absorptivity=absorptivity)
164
- return {
165
- "success": True,
166
- "model": str(model),
167
- "diagnostics": model.diagnostics
 
 
 
 
 
 
 
168
  }
 
169
  except Exception as e:
170
- return {"success": False, "error": str(e)}
 
171
 
172
- @mcp.tool(name="create_grey_radiation_model", description="创建一个灰气辐射列模型")
173
- def create_grey_radiation_model() -> dict:
 
 
174
  """
175
- 创建一个灰气辐射列模型。
176
 
177
- Returns:
178
- - dict: 包含模型信息的字典。
 
 
179
  """
180
  try:
181
- from climlab.model.column import GreyRadiationModel
182
- model = GreyRadiationModel()
183
- return {
184
- "success": True,
185
- "model": str(model),
186
- "state": model.state,
187
- "diagnostics": list(model.diagnostics.keys())
188
  }
 
189
  except Exception as e:
190
- return {"success": False, "error": str(e)}
 
191
 
192
- @mcp.tool(name="create_advection_diffusion", description="创建一个对流扩散模型")
193
- def create_advection_diffusion() -> dict:
194
  """
195
- 创建一个对流扩散模型。
196
 
197
- Returns:
198
- - dict: 包含模型信息的字典。
 
199
  """
200
  try:
201
- from climlab.dynamics.advection_diffusion import AdvectionDiffusion
202
- model = AdvectionDiffusion()
203
- return {
204
- "success": True,
205
- "model": str(model),
206
- "diagnostics": model.diagnostics
 
 
 
207
  }
 
208
  except Exception as e:
209
- return {"success": False, "error": str(e)}
 
210
 
211
- @mcp.tool(name="create_surface_flux", description="创建一个地表湍流热量和水分通量模型")
212
- def create_surface_flux() -> dict:
 
 
213
  """
214
- 创建一个地表湍流热量和水分通量模型。
 
215
 
216
- Returns:
217
- - dict: 包含模型信息的字典。
 
 
218
  """
219
  try:
220
- from climlab.surface.turbulent import SensibleHeatFlux, LatentHeatFlux
221
- sensible_flux = SensibleHeatFlux()
222
- latent_flux = LatentHeatFlux()
223
- return {
224
- "success": True,
225
- "sensible_flux": str(sensible_flux),
226
- "latent_flux": str(latent_flux)
 
 
 
 
 
 
227
  }
 
228
  except Exception as e:
229
- return {"success": False, "error": str(e)}
 
230
 
231
- @mcp.tool(name="create_time_dependent_process", description="创建一个时间相关的过程模型")
232
- def create_time_dependent_process() -> dict:
 
233
  """
234
- 创建一个时间相关的过程模型。
235
 
236
- Returns:
237
- - dict: 包含模型信息的字典。
 
 
 
 
238
  """
239
  try:
240
- from climlab.process.process import TimeDependentProcess
241
- model = TimeDependentProcess()
242
- return {
243
- "success": True,
244
- "model": str(model),
245
- "state": model.state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
 
247
  except Exception as e:
248
- return {"success": False, "error": str(e)}
 
249
 
250
- @mcp.tool(name="get_physical_constants", description="获取大气和海洋的物理常量")
251
- def get_physical_constants() -> dict:
 
 
 
252
  """
253
- 获取大气和海洋的物理常量。
254
 
255
- Returns:
256
- - dict: 包含常量的字典。
 
 
 
257
  """
258
  try:
259
- from climlab.utils.constants import a, Lhvap, Lhsub, Lhfus, cp, Rd, kappa, Rv, cpv, eps, Omega, g, kBoltzmann, c_light, hPlanck, sigma, S0, ps, rho_w, cw
260
- constants = {
261
- "Earth_radius": a,
262
- "Latent_heat_vaporization": Lhvap,
263
- "Latent_heat_sublimation": Lhsub,
264
- "Latent_heat_fusion": Lhfus,
265
- "Specific_heat_dry_air": cp,
266
- "Gas_constant_dry_air": Rd,
267
- "Kappa": kappa,
268
- "Gas_constant_water_vapor": Rv,
269
- "Specific_heat_water_vapor": cpv,
270
- "Epsilon": eps,
271
- "Earth_rotation_rate": Omega,
272
- "Gravitational_acceleration": g,
273
- "Boltzmann_constant": kBoltzmann,
274
- "Speed_of_light": c_light,
275
- "Planck_constant": hPlanck,
276
- "Stefan_Boltzmann_constant": sigma,
277
- "Solar_constant": S0,
278
- "Surface_pressure": ps,
279
- "Density_of_water": rho_w,
280
- "Specific_heat_liquid_water": cw
281
  }
282
- return {"success": True, "constants": constants}
283
  except Exception as e:
284
- return {"success": False, "error": str(e)}
285
 
286
- @mcp.tool(name="calculate_potential_temperature", description="计算大气的位温")
287
- def calculate_potential_temperature(T: float, p: float) -> dict:
288
- """
289
- 计算大气的位温。
290
 
291
- Parameters:
292
- - T: 温度 (单位: K)。
293
- - p: 压力 (单位: hPa)。
 
 
 
294
 
295
- Returns:
296
- - dict: 包含位温的字典。
 
 
 
 
297
  """
298
  try:
299
- from climlab.utils.thermo import potential_temperature
300
- theta = potential_temperature(T, p)
301
- return {"success": True, "potential_temperature": theta}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  except Exception as e:
303
- return {"success": False, "error": str(e)}
 
 
 
304
 
305
- @mcp.tool(name="calculate_clausius_clapeyron", description="计算克劳修斯-克拉珀龙方程")
306
- def calculate_clausius_clapeyron(T: float) -> dict:
307
  """
308
- 计算克劳修斯-克拉珀龙方程。
 
309
 
310
- Parameters:
311
- - T: 温度 (单位: K)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- Returns:
314
- - dict: 包含计算结果的字典。
 
 
 
315
  """
316
  try:
317
- from climlab.utils.thermo import clausius_clapeyron
318
- result = clausius_clapeyron(T)
319
- return {"success": True, "result": result}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  except Exception as e:
321
- return {"success": False, "error": str(e)}
 
322
 
323
- @mcp.tool(name="test_ebm_model", description="测试 EBM 模型的创建和运行")
324
- def test_ebm_model() -> dict:
 
 
325
  """
326
- 测试 EBM 模型的创建和运行。
327
 
328
- Returns:
329
- - dict: 测试结果。
330
  """
331
  try:
332
- import numpy as np
333
- import climlab
334
- old_Jan1 = np.datetime64('2025-03-20T09:01') - np.timedelta64(80, 'D')
335
- model = climlab.EBM_seasonal(initial_time=old_Jan1, water_depth=10.)
336
- model.integrate_years(1)
337
- return {
338
- "success": True,
339
- "state": model.state,
340
- "diagnostics": model.diagnostics
 
 
 
 
 
 
341
  }
 
342
  except Exception as e:
343
- return {"success": False, "error": str(e)}
 
 
 
344
 
345
- @mcp.tool(name="test_rcm_model", description="测试 RCM 模型的创建和运行")
346
- def test_rcm_model() -> dict:
347
  """
348
- 测试 RCM 模型的创建和运行。
349
 
350
- Returns:
351
- - dict: 测试结果。
 
352
  """
353
  try:
354
- import numpy as np
355
- import climlab
356
- state = climlab.column_state(num_lev=40, num_lat=1, water_depth=5.)
357
- h2o = climlab.radiation.ManabeWaterVapor(state=state, name='H2O')
358
- convadj = climlab.convection.ConvectiveAdjustment(state=state, name='ConvectiveAdjustment', adj_lapse_rate=6.5)
359
- rad = climlab.radiation.RRTMG(state=state, albedo=0.2, specific_humidity=h2o.q, name='Radiation')
360
- rcm = climlab.couple([h2o, convadj, rad], name='RCM')
361
- rcm.step_forward()
362
- return {
363
- "success": True,
364
- "state": rcm.state,
365
- "diagnostics": rcm.diagnostics
 
 
 
366
  }
 
367
  except Exception as e:
368
- return {"success": False, "error": str(e)}
369
 
370
- @mcp.tool(name="test_grey_radiation_model", description="测试灰气辐射模型的创建和运行")
371
- def test_grey_radiation_model() -> dict:
 
372
  """
373
- 测试灰气辐射模型的创建和运行。
374
 
375
- Returns:
376
- - dict: 测试结果。
 
 
377
  """
378
  try:
379
- import numpy as np
380
- import climlab
381
- old_Jan1 = np.datetime64('2025-03-20T09:01') - np.timedelta64(80, 'D')
382
- model = climlab.GreyRadiationModel(num_lev=30, num_lat=90)
383
- model.integrate_years(1)
384
- return {
385
- "success": True,
386
- "state": model.state,
387
- "diagnostics": model.diagnostics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
 
389
  except Exception as e:
390
- return {"success": False, "error": str(e)}
 
391
 
392
- # 创建 FastMCP 应用实例
393
  def create_app() -> FastMCP:
394
  """
395
- 创建并返回 FastMCP 应用实例。
396
 
397
- Returns:
398
- - FastMCP: FastMCP 应用实例。
399
  """
400
  return mcp
 
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+
5
+ # Add the local source directory to sys.path
6
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
7
+ if source_path not in sys.path:
8
+ sys.path.insert(0, source_path)
9
+
10
  from fastmcp import FastMCP
11
 
12
+ # Import core modules from climlab using the correct API
13
+ import climlab
14
+ from climlab.domain.domain import single_column, zonal_mean_column, box_model_domain, zonal_mean_surface
15
+ from climlab.domain.initial import column_state, surface_state
16
+ from climlab.domain.field import Field, global_mean
17
+ from climlab.model.ebm import EBM, EBM_annual, EBM_seasonal
18
+ from climlab.model.column import GreyRadiationModel, RadiativeConvectiveModel, BandRCModel
19
+ from climlab.radiation.insolation import P2Insolation, AnnualMeanInsolation, DailyInsolation, FixedInsolation
20
+ from climlab.radiation.aplusbt import AplusBT
21
+ from climlab.surface.albedo import ConstantAlbedo, P2Albedo, StepFunctionAlbedo
22
+ from climlab.solar.insolation import daily_insolation, annual_insolation
23
+ from climlab import constants as const
24
+
25
+ # Create the FastMCP service application
26
  mcp = FastMCP("climlab_service")
27
 
28
+
29
+ # ==================== Domain Tools ====================
30
+
31
+ @mcp.tool(name="create_column_state", description="Create a column state for climate modeling with atmospheric and surface temperatures")
32
+ def create_column_state_tool(num_lev: int = 30, num_lat: int = 1, water_depth: float = 1.0) -> dict:
33
  """
34
+ Create a column state for climate modeling.
35
 
36
+ :param num_lev: The number of vertical levels (default: 30).
37
+ :param num_lat: The number of latitude points (default: 1).
38
+ :param water_depth: Depth of the slab ocean in meters (default: 1.0).
39
+ :return: A dictionary containing success status and the state object.
40
  """
41
  try:
42
+ state = column_state(num_lev=num_lev, num_lat=num_lat, water_depth=water_depth)
43
+ result = {
44
+ "Ts": {
45
+ "shape": list(state['Ts'].shape),
46
+ "values": state['Ts'].tolist(),
47
+ "domain_type": state['Ts'].domain.domain_type,
48
+ },
49
+ "Tatm": {
50
+ "shape": list(state['Tatm'].shape),
51
+ "min": float(state['Tatm'].min()),
52
+ "max": float(state['Tatm'].max()),
53
+ "domain_type": state['Tatm'].domain.domain_type,
54
+ },
55
+ "num_lev": num_lev,
56
+ "num_lat": num_lat,
57
+ }
58
+ return {"success": True, "result": result, "error": None}
59
  except Exception as e:
60
+ return {"success": False, "result": None, "error": str(e)}
61
+
62
 
63
+ @mcp.tool(name="create_surface_state", description="Create a surface state for EBM with latitude-dependent initial temperature")
64
+ def create_surface_state_tool(num_lat: int = 90, water_depth: float = 10.0, T0: float = 12.0, T2: float = -40.0) -> dict:
65
  """
66
+ Create a surface state for Energy Balance Models.
67
 
68
+ :param num_lat: Number of latitude points (default: 90).
69
+ :param water_depth: Depth of the slab ocean in meters (default: 10.0).
70
+ :param T0: Global-mean initial temperature in °C (default: 12.0).
71
+ :param T2: 2nd Legendre coefficient for equator-to-pole gradient (default: -40.0).
72
+ :return: A dictionary containing success status and state information.
73
  """
74
  try:
75
+ state = surface_state(num_lat=num_lat, water_depth=water_depth, T0=T0, T2=T2)
76
+ Ts = state['Ts']
77
+ lat = Ts.domain.axes['lat'].points
78
+ result = {
79
+ "shape": list(Ts.shape),
80
+ "latitudes": lat.tolist(),
81
+ "temperatures": np.squeeze(Ts).tolist(),
82
+ "global_mean_temperature": float(global_mean(Ts)),
83
+ "equator_temperature": float(Ts[len(Ts)//2]) if len(Ts) > 1 else float(Ts[0]),
84
+ "pole_temperature": float(Ts[0]),
85
  }
86
+ return {"success": True, "result": result, "error": None}
87
  except Exception as e:
88
+ return {"success": False, "result": None, "error": str(e)}
89
 
 
 
 
 
90
 
91
+ # ==================== EBM Tools ====================
92
+
93
+ @mcp.tool(name="create_ebm", description="Create an Energy Balance Model with diffusive heat transport")
94
+ def create_ebm_tool(num_lat: int = 90, S0: float = 1365.2, A: float = 210.0, B: float = 2.0,
95
+ D: float = 0.555, a0: float = 0.3, a2: float = 0.078, ai: float = 0.62,
96
+ Tf: float = -10.0, water_depth: float = 10.0) -> dict:
97
+ """
98
+ Create an Energy Balance Model (EBM) with specified parameters.
99
 
100
+ :param num_lat: Number of latitude points (default: 90).
101
+ :param S0: Solar constant in W/m² (default: 1365.2).
102
+ :param A: OLR parameter A in W/m² (default: 210.0).
103
+ :param B: OLR parameter B in W/m²/°C (default: 2.0).
104
+ :param D: Diffusion parameter in W/m²/°C (default: 0.555).
105
+ :param a0: Base albedo coefficient (default: 0.3).
106
+ :param a2: Second Legendre polynomial coefficient (default: 0.078).
107
+ :param ai: Ice albedo value (default: 0.62).
108
+ :param Tf: Freezing temperature in °C (default: -10.0).
109
+ :param water_depth: Slab ocean depth in meters (default: 10.0).
110
+ :return: A dictionary containing success status and model info.
111
  """
112
  try:
113
+ model = EBM(num_lat=num_lat, S0=S0, A=A, B=B, D=D,
114
+ a0=a0, a2=a2, ai=ai, Tf=Tf, water_depth=water_depth)
115
+ lat = model.lat
116
+ result = {
117
+ "model_type": "EBM",
118
+ "parameters": {
119
+ "num_lat": num_lat,
120
+ "S0": S0,
121
+ "A": A,
122
+ "B": B,
123
+ "D": D,
124
+ "a0": a0,
125
+ "a2": a2,
126
+ "ai": ai,
127
+ "Tf": Tf,
128
+ "water_depth": water_depth,
129
+ },
130
+ "state_variables": list(model.state.keys()),
131
+ "subprocesses": list(model.subprocess.keys()),
132
+ "latitudes": lat.tolist(),
133
+ "initial_Ts": np.squeeze(model.Ts).tolist(),
134
+ "timestep_seconds": float(model.timestep),
135
  }
136
+ return {"success": True, "result": result, "error": None}
137
  except Exception as e:
138
+ return {"success": False, "result": None, "error": str(e)}
139
 
140
+
141
+ @mcp.tool(name="run_ebm_to_equilibrium", description="Run an Energy Balance Model to equilibrium and return climate statistics")
142
+ def run_ebm_to_equilibrium_tool(num_lat: int = 90, num_years: float = 5.0,
143
+ S0: float = 1365.2, A: float = 210.0, B: float = 2.0,
144
+ D: float = 0.555, a0: float = 0.3, a2: float = 0.078,
145
+ ai: float = 0.62, Tf: float = -10.0) -> dict:
146
  """
147
+ Run an Energy Balance Model to equilibrium.
148
 
149
+ :param num_lat: Number of latitude points (default: 90).
150
+ :param num_years: Number of years to integrate (default: 5.0).
151
+ :param S0: Solar constant in W/m² (default: 1365.2).
152
+ :param A: OLR parameter A in W/m² (default: 210.0).
153
+ :param B: OLR parameter B in W/m²/°C (default: 2.0).
154
+ :param D: Diffusion parameter in W/m²/°C (default: 0.555).
155
+ :param a0: Base albedo coefficient (default: 0.3).
156
+ :param a2: Second Legendre polynomial coefficient (default: 0.078).
157
+ :param ai: Ice albedo value (default: 0.62).
158
+ :param Tf: Freezing temperature in °C (default: -10.0).
159
+ :return: A dictionary containing equilibrium results.
160
  """
161
  try:
162
+ model = EBM(num_lat=num_lat, S0=S0, A=A, B=B, D=D, a0=a0, a2=a2, ai=ai, Tf=Tf)
163
+ model.integrate_years(num_years)
164
+ Ts = model.Ts
165
+ lat = model.lat
166
+
167
+ # Get heat transport if available
168
+ heat_transport = None
169
+ if hasattr(model.subprocess.get('diffusion', None), 'heat_transport'):
170
+ heat_transport = model.subprocess['diffusion'].heat_transport.tolist()
171
+
172
+ # Get OLR
173
+ OLR = None
174
+ if hasattr(model, 'OLR'):
175
+ OLR = np.squeeze(model.OLR).tolist()
176
+
177
+ # Get ASR
178
+ ASR = None
179
+ if hasattr(model, 'ASR'):
180
+ ASR = np.squeeze(model.ASR).tolist()
181
+
182
+ # Find ice edge latitude
183
+ ice_lat = None
184
+ if hasattr(model, 'icelat'):
185
+ ice_lat = float(model.icelat) if np.isfinite(model.icelat) else None
186
+
187
+ result = {
188
+ "global_mean_temperature": float(global_mean(Ts)),
189
+ "max_temperature": float(Ts.max()),
190
+ "min_temperature": float(Ts.min()),
191
+ "equator_temperature": float(Ts[len(Ts)//2]) if len(Ts) > 1 else float(Ts[0]),
192
+ "pole_temperature": float(Ts[0]),
193
+ "latitudes": lat.tolist(),
194
+ "temperature_profile": np.squeeze(Ts).tolist(),
195
+ "ice_edge_latitude": ice_lat,
196
+ "OLR": OLR,
197
+ "ASR": ASR,
198
+ "heat_transport_PW": heat_transport,
199
+ "integrated_years": num_years,
200
+ "energy_balance": float(global_mean(model.ASR - model.OLR)) if hasattr(model, 'ASR') else None,
201
+ }
202
+ return {"success": True, "result": result, "error": None}
203
  except Exception as e:
204
+ return {"success": False, "result": None, "error": str(e)}
205
 
 
 
 
 
206
 
207
+ @mcp.tool(name="run_seasonal_ebm", description="Run a seasonal Energy Balance Model with time-varying insolation")
208
+ def run_seasonal_ebm_tool(num_lat: int = 90, num_years: float = 5.0,
209
+ water_depth: float = 10.0) -> dict:
210
+ """
211
+ Run a seasonal Energy Balance Model with realistic seasonally varying insolation.
212
 
213
+ :param num_lat: Number of latitude points (default: 90).
214
+ :param num_years: Number of years to integrate (default: 5.0).
215
+ :param water_depth: Slab ocean depth in meters (default: 10.0).
216
+ :return: A dictionary containing seasonal climate results.
217
  """
218
  try:
219
+ model = EBM_seasonal(num_lat=num_lat, water_depth=water_depth)
220
+ model.integrate_years(num_years)
221
+ Ts = model.Ts
222
+ lat = model.lat
223
+
224
+ result = {
225
+ "global_mean_temperature": float(global_mean(Ts)),
226
+ "max_temperature": float(Ts.max()),
227
+ "min_temperature": float(Ts.min()),
228
+ "latitudes": lat.tolist(),
229
+ "temperature_profile": np.squeeze(Ts).tolist(),
230
+ "integrated_years": num_years,
231
+ "model_type": "EBM_seasonal",
 
 
 
232
  }
233
+ return {"success": True, "result": result, "error": None}
234
  except Exception as e:
235
+ return {"success": False, "result": None, "error": str(e)}
236
 
 
 
 
 
237
 
238
+ # ==================== Insolation Tools ====================
 
239
 
240
+ @mcp.tool(name="calculate_daily_insolation", description="Calculate daily average insolation given latitude and day of year")
241
+ def calculate_daily_insolation_tool(lat: float, day: int, S0: float = 1365.2,
242
+ ecc: float = 0.017236, obliquity: float = 23.446,
243
+ long_peri: float = 281.37) -> dict:
244
+ """
245
+ Calculate daily average insolation at given latitude and day of year.
246
+
247
+ :param lat: Latitude in degrees (-90 to 90).
248
+ :param day: Calendar day (1-365), day 1 is January 1st.
249
+ :param S0: Solar constant in W/m² (default: 1365.2).
250
+ :param ecc: Orbital eccentricity (default: 0.017236 for present-day).
251
+ :param obliquity: Obliquity angle in degrees (default: 23.446).
252
+ :param long_peri: Longitude of perihelion in degrees (default: 281.37).
253
+ :return: A dictionary containing daily average insolation in W/m².
254
  """
255
  try:
256
+ orb = {'ecc': ecc, 'obliquity': obliquity, 'long_peri': long_peri}
257
+ insolation = daily_insolation(lat=lat, day=day, orb=orb, S0=S0)
258
+
259
+ result = {
260
+ "latitude": lat,
261
+ "day_of_year": day,
262
+ "insolation_W_m2": float(insolation),
263
+ "solar_constant": S0,
264
+ "orbital_parameters": orb,
265
  }
266
+ return {"success": True, "result": result, "error": None}
267
  except Exception as e:
268
+ return {"success": False, "result": None, "error": str(e)}
269
+
270
 
271
+ @mcp.tool(name="calculate_annual_mean_insolation", description="Calculate annual mean insolation as a function of latitude")
272
+ def calculate_annual_mean_insolation_tool(num_lat: int = 90, S0: float = 1365.2,
273
+ ecc: float = 0.017236, obliquity: float = 23.446,
274
+ long_peri: float = 281.37) -> dict:
275
  """
276
+ Calculate annual mean insolation for a range of latitudes.
277
 
278
+ :param num_lat: Number of latitude points (default: 90).
279
+ :param S0: Solar constant in W/m² (default: 1365.2).
280
+ :param ecc: Orbital eccentricity (default: 0.017236).
281
+ :param obliquity: Obliquity angle in degrees (default: 23.446).
282
+ :param long_peri: Longitude of perihelion in degrees (default: 281.37).
283
+ :return: A dictionary containing annual mean insolation profile.
284
  """
285
  try:
286
+ lat = np.linspace(-90, 90, num_lat)
287
+ orb = {'ecc': ecc, 'obliquity': obliquity, 'long_peri': long_peri}
288
+ insolation = annual_insolation(lat=lat, orb=orb, S0=S0)
289
+
290
+ result = {
291
+ "latitudes": lat.tolist(),
292
+ "insolation_W_m2": insolation.tolist() if hasattr(insolation, 'tolist') else [float(insolation)],
293
+ "global_mean_insolation": float(np.mean(insolation * np.cos(np.deg2rad(lat))) / np.mean(np.cos(np.deg2rad(lat)))),
294
+ "equator_insolation": float(insolation[num_lat//2]) if num_lat > 1 else float(insolation),
295
+ "pole_insolation": float(insolation[0]) if num_lat > 1 else float(insolation),
296
+ "solar_constant": S0,
297
  }
298
+ return {"success": True, "result": result, "error": None}
299
  except Exception as e:
300
+ return {"success": False, "result": None, "error": str(e)}
301
 
 
 
 
 
302
 
303
+ @mcp.tool(name="calculate_insolation_seasonal_cycle", description="Calculate the seasonal cycle of insolation at a single latitude")
304
+ def calculate_insolation_seasonal_cycle_tool(lat: float, S0: float = 1365.2,
305
+ ecc: float = 0.017236, obliquity: float = 23.446,
306
+ long_peri: float = 281.37) -> dict:
307
+ """
308
+ Calculate the seasonal cycle of daily insolation at a given latitude.
309
 
310
+ :param lat: Latitude in degrees (-90 to 90).
311
+ :param S0: Solar constant in W/m² (default: 1365.2).
312
+ :param ecc: Orbital eccentricity (default: 0.017236).
313
+ :param obliquity: Obliquity angle in degrees (default: 23.446).
314
+ :param long_peri: Longitude of perihelion in degrees (default: 281.37).
315
+ :return: A dictionary containing the seasonal cycle of insolation.
316
  """
317
  try:
318
+ days = np.arange(1, 366)
319
+ orb = {'ecc': ecc, 'obliquity': obliquity, 'long_peri': long_peri}
320
+ insolation = np.array([float(daily_insolation(lat=lat, day=d, orb=orb, S0=S0)) for d in days])
321
+
322
+ result = {
323
+ "latitude": lat,
324
+ "days": days.tolist(),
325
+ "insolation_W_m2": insolation.tolist(),
326
+ "annual_mean": float(np.mean(insolation)),
327
+ "max_insolation": float(np.max(insolation)),
328
+ "min_insolation": float(np.min(insolation)),
329
+ "day_of_max": int(days[np.argmax(insolation)]),
330
+ "day_of_min": int(days[np.argmin(insolation)]),
331
  }
332
+ return {"success": True, "result": result, "error": None}
333
  except Exception as e:
334
+ return {"success": False, "result": None, "error": str(e)}
335
+
336
 
337
+ # ==================== Radiation Tools ====================
338
+
339
+ @mcp.tool(name="calculate_olr_aplusbt", description="Calculate Outgoing Longwave Radiation using A+BT parameterization")
340
+ def calculate_olr_aplusbt_tool(temperature: float, A: float = 210.0, B: float = 2.0) -> dict:
341
  """
342
+ Calculate Outgoing Longwave Radiation using the simple A+BT parameterization.
343
 
344
+ :param temperature: Surface temperature in °C.
345
+ :param A: OLR parameter A in W/m² (default: 210.0).
346
+ :param B: OLR parameter B in W/m²/°C (default: 2.0).
347
+ :return: A dictionary containing the OLR value.
348
  """
349
  try:
350
+ OLR = A + B * temperature
351
+ result = {
352
+ "temperature_C": temperature,
353
+ "OLR_W_m2": float(OLR),
354
+ "A": A,
355
+ "B": B,
356
+ "description": f"OLR = {A} + {B} * T = {OLR:.2f} W/m²",
357
  }
358
+ return {"success": True, "result": result, "error": None}
359
  except Exception as e:
360
+ return {"success": False, "result": None, "error": str(e)}
361
+
362
 
363
+ @mcp.tool(name="calculate_stefan_boltzmann_radiation", description="Calculate blackbody radiation using Stefan-Boltzmann law")
364
+ def calculate_stefan_boltzmann_radiation_tool(temperature_K: float, emissivity: float = 1.0) -> dict:
365
  """
366
+ Calculate blackbody (or greybody) radiation using Stefan-Boltzmann law.
367
 
368
+ :param temperature_K: Temperature in Kelvin.
369
+ :param emissivity: Surface emissivity (0-1, default: 1.0 for blackbody).
370
+ :return: A dictionary containing the radiation flux.
371
  """
372
  try:
373
+ sigma = const.sigma # Stefan-Boltzmann constant
374
+ radiation = emissivity * sigma * temperature_K**4
375
+
376
+ result = {
377
+ "temperature_K": temperature_K,
378
+ "temperature_C": temperature_K - 273.15,
379
+ "emissivity": emissivity,
380
+ "radiation_W_m2": float(radiation),
381
+ "stefan_boltzmann_constant": sigma,
382
  }
383
+ return {"success": True, "result": result, "error": None}
384
  except Exception as e:
385
+ return {"success": False, "result": None, "error": str(e)}
386
+
387
 
388
+ # ==================== Albedo Tools ====================
389
+
390
+ @mcp.tool(name="calculate_p2_albedo", description="Calculate latitude-dependent albedo using P2 Legendre polynomial")
391
+ def calculate_p2_albedo_tool(num_lat: int = 90, a0: float = 0.33, a2: float = 0.25) -> dict:
392
  """
393
+ Calculate latitude-dependent albedo using second-order Legendre polynomial.
394
+ α(φ) = a0 + a2 * P2(sin(φ)), where P2(x) = (3x² - 1)/2
395
 
396
+ :param num_lat: Number of latitude points (default: 90).
397
+ :param a0: Base albedo parameter (default: 0.33).
398
+ :param a2: Second Legendre coefficient (default: 0.25).
399
+ :return: A dictionary containing the albedo profile.
400
  """
401
  try:
402
+ from climlab.utils.legendre import P2
403
+ lat = np.linspace(-90, 90, num_lat)
404
+ phi = np.deg2rad(lat)
405
+ albedo = a0 + a2 * P2(np.sin(phi))
406
+
407
+ result = {
408
+ "latitudes": lat.tolist(),
409
+ "albedo": albedo.tolist(),
410
+ "global_mean_albedo": float(np.average(albedo, weights=np.cos(phi))),
411
+ "equator_albedo": float(albedo[num_lat//2]),
412
+ "pole_albedo": float(albedo[0]),
413
+ "a0": a0,
414
+ "a2": a2,
415
  }
416
+ return {"success": True, "result": result, "error": None}
417
  except Exception as e:
418
+ return {"success": False, "result": None, "error": str(e)}
419
+
420
 
421
+ @mcp.tool(name="calculate_ice_albedo_feedback", description="Calculate ice-albedo feedback with temperature-dependent ice line")
422
+ def calculate_ice_albedo_feedback_tool(temperatures: list, Tf: float = -10.0,
423
+ a0: float = 0.3, a2: float = 0.078, ai: float = 0.62) -> dict:
424
  """
425
+ Calculate albedo with ice-albedo feedback based on temperature.
426
 
427
+ :param temperatures: List of temperatures in °C at each latitude.
428
+ :param Tf: Freezing temperature threshold in °C (default: -10.0).
429
+ :param a0: Unfrozen base albedo (default: 0.3).
430
+ :param a2: Second Legendre coefficient for unfrozen albedo (default: 0.078).
431
+ :param ai: Ice albedo value (default: 0.62).
432
+ :return: A dictionary containing the albedo with ice-albedo feedback.
433
  """
434
  try:
435
+ from climlab.utils.legendre import P2
436
+ T = np.array(temperatures)
437
+ num_lat = len(T)
438
+ lat = np.linspace(-90, 90, num_lat)
439
+ phi = np.deg2rad(lat)
440
+
441
+ # Calculate unfrozen albedo using P2
442
+ albedo_unfrozen = a0 + a2 * P2(np.sin(phi))
443
+
444
+ # Apply ice-albedo feedback: ice where T < Tf
445
+ albedo = np.where(T < Tf, ai, albedo_unfrozen)
446
+
447
+ # Find ice edge latitude
448
+ ice_mask = T < Tf
449
+ if np.any(ice_mask) and not np.all(ice_mask):
450
+ # Find the latitude where ice starts
451
+ ice_lats = np.abs(lat[ice_mask])
452
+ ice_edge = float(np.min(ice_lats))
453
+ elif np.all(ice_mask):
454
+ ice_edge = 0.0 # Snowball Earth
455
+ else:
456
+ ice_edge = 90.0 # No ice
457
+
458
+ result = {
459
+ "latitudes": lat.tolist(),
460
+ "temperatures": temperatures,
461
+ "albedo": albedo.tolist(),
462
+ "ice_mask": ice_mask.tolist(),
463
+ "ice_edge_latitude": ice_edge,
464
+ "global_mean_albedo": float(np.average(albedo, weights=np.cos(phi))),
465
+ "parameters": {"Tf": Tf, "a0": a0, "a2": a2, "ai": ai},
466
  }
467
+ return {"success": True, "result": result, "error": None}
468
  except Exception as e:
469
+ return {"success": False, "result": None, "error": str(e)}
470
+
471
 
472
+ # ==================== Column Model Tools ====================
473
+
474
+ @mcp.tool(name="run_grey_radiation_model", description="Run a grey-gas radiative column model")
475
+ def run_grey_radiation_model_tool(num_lev: int = 30, num_years: float = 2.0,
476
+ albedo_sfc: float = 0.299, Q: float = 341.3) -> dict:
477
  """
478
+ Run a grey-gas radiative column model to equilibrium.
479
 
480
+ :param num_lev: Number of vertical levels (default: 30).
481
+ :param num_years: Number of years to integrate (default: 2.0).
482
+ :param albedo_sfc: Surface albedo (default: 0.299).
483
+ :param Q: Insolation in W/m² (default: 341.3).
484
+ :return: A dictionary containing the equilibrium temperature profile.
485
  """
486
  try:
487
+ model = GreyRadiationModel(num_lev=num_lev, albedo_sfc=albedo_sfc, Q=Q)
488
+ model.integrate_years(num_years)
489
+
490
+ Ts = float(np.squeeze(model.Ts))
491
+ Tatm = np.squeeze(model.Tatm).tolist()
492
+ lev = model.lev.tolist()
493
+
494
+ result = {
495
+ "surface_temperature_K": Ts,
496
+ "surface_temperature_C": Ts - 273.15,
497
+ "atmospheric_temperature_K": Tatm,
498
+ "pressure_levels_hPa": lev,
499
+ "OLR_W_m2": float(np.squeeze(model.OLR)),
500
+ "ASR_W_m2": float(np.squeeze(model.ASR)),
501
+ "num_levels": num_lev,
502
+ "integrated_years": num_years,
 
 
 
 
 
 
503
  }
504
+ return {"success": True, "result": result, "error": None}
505
  except Exception as e:
506
+ return {"success": False, "result": None, "error": str(e)}
507
 
 
 
 
 
508
 
509
+ @mcp.tool(name="run_radiative_convective_model", description="Run a radiative-convective column model with convective adjustment")
510
+ def run_radiative_convective_model_tool(num_lev: int = 30, num_years: float = 5.0,
511
+ adj_lapse_rate: float = 6.5,
512
+ albedo_sfc: float = 0.299, Q: float = 341.3) -> dict:
513
+ """
514
+ Run a radiative-convective equilibrium model.
515
 
516
+ :param num_lev: Number of vertical levels (default: 30).
517
+ :param num_years: Number of years to integrate (default: 5.0).
518
+ :param adj_lapse_rate: Convective adjustment lapse rate in K/km (default: 6.5).
519
+ :param albedo_sfc: Surface albedo (default: 0.299).
520
+ :param Q: Insolation in W/m² (default: 341.3).
521
+ :return: A dictionary containing the RCE temperature profile.
522
  """
523
  try:
524
+ model = RadiativeConvectiveModel(num_lev=num_lev, adj_lapse_rate=adj_lapse_rate,
525
+ albedo_sfc=albedo_sfc, Q=Q)
526
+ model.integrate_years(num_years)
527
+
528
+ Ts = float(np.squeeze(model.Ts))
529
+ Tatm = np.squeeze(model.Tatm).tolist()
530
+ lev = model.lev.tolist()
531
+
532
+ result = {
533
+ "surface_temperature_K": Ts,
534
+ "surface_temperature_C": Ts - 273.15,
535
+ "atmospheric_temperature_K": Tatm,
536
+ "pressure_levels_hPa": lev,
537
+ "OLR_W_m2": float(np.squeeze(model.OLR)),
538
+ "ASR_W_m2": float(np.squeeze(model.ASR)),
539
+ "adj_lapse_rate_K_km": adj_lapse_rate,
540
+ "num_levels": num_lev,
541
+ "integrated_years": num_years,
542
+ }
543
+ return {"success": True, "result": result, "error": None}
544
  except Exception as e:
545
+ return {"success": False, "result": None, "error": str(e)}
546
+
547
+
548
+ # ==================== Climate Sensitivity Tools ====================
549
 
550
+ @mcp.tool(name="calculate_climate_sensitivity", description="Calculate equilibrium climate sensitivity from EBM parameters")
551
+ def calculate_climate_sensitivity_tool(B: float = 2.0, f: float = 0.0) -> dict:
552
  """
553
+ Calculate equilibrium climate sensitivity (ECS) from feedback parameters.
554
+ ECS = -ΔF / λ, where λ = B - f is the net feedback parameter.
555
 
556
+ :param B: Planck feedback parameter in W/m²/°C (default: 2.0).
557
+ :param f: Sum of other feedback parameters in W/m²/°C (default: 0.0).
558
+ :return: A dictionary containing the climate sensitivity.
559
+ """
560
+ try:
561
+ # Net feedback parameter
562
+ lambda_net = B - f
563
+
564
+ # Standard CO2 doubling forcing
565
+ delta_F_2xCO2 = 3.7 # W/m²
566
+
567
+ # Equilibrium climate sensitivity
568
+ if lambda_net > 0:
569
+ ECS = delta_F_2xCO2 / lambda_net
570
+ else:
571
+ ECS = float('inf') # Runaway climate
572
+
573
+ result = {
574
+ "Planck_feedback_B": B,
575
+ "other_feedbacks_f": f,
576
+ "net_feedback_lambda": lambda_net,
577
+ "CO2_doubling_forcing_W_m2": delta_F_2xCO2,
578
+ "equilibrium_climate_sensitivity_C": float(ECS),
579
+ "description": f"For λ = {lambda_net:.2f} W/m²/°C, ECS = {ECS:.2f} °C per CO2 doubling",
580
+ }
581
+ return {"success": True, "result": result, "error": None}
582
+ except Exception as e:
583
+ return {"success": False, "result": None, "error": str(e)}
584
+
585
+
586
+ @mcp.tool(name="run_ebm_co2_doubling", description="Run EBM to simulate global warming from CO2 doubling")
587
+ def run_ebm_co2_doubling_tool(num_lat: int = 90, num_years: float = 50.0,
588
+ A_control: float = 210.0, forcing: float = 3.7) -> dict:
589
+ """
590
+ Run EBM experiment comparing control run with CO2 doubling scenario.
591
 
592
+ :param num_lat: Number of latitude points (default: 90).
593
+ :param num_years: Number of years to integrate (default: 50.0).
594
+ :param A_control: Control OLR parameter A in W/m² (default: 210.0).
595
+ :param forcing: Radiative forcing from CO2 doubling in W/m² (default: 3.7).
596
+ :return: A dictionary containing control and perturbed climate states.
597
  """
598
  try:
599
+ # Control run
600
+ model_ctrl = EBM(num_lat=num_lat, A=A_control)
601
+ model_ctrl.integrate_years(num_years)
602
+ T_ctrl = float(global_mean(model_ctrl.Ts))
603
+
604
+ # Perturbed run (CO2 doubling reduces OLR, equivalent to reducing A)
605
+ model_2xCO2 = EBM(num_lat=num_lat, A=A_control - forcing)
606
+ model_2xCO2.integrate_years(num_years)
607
+ T_2xCO2 = float(global_mean(model_2xCO2.Ts))
608
+
609
+ warming = T_2xCO2 - T_ctrl
610
+
611
+ result = {
612
+ "control_global_mean_T_C": T_ctrl,
613
+ "2xCO2_global_mean_T_C": T_2xCO2,
614
+ "equilibrium_warming_C": warming,
615
+ "forcing_W_m2": forcing,
616
+ "effective_climate_sensitivity_C": warming,
617
+ "control_temperature_profile": np.squeeze(model_ctrl.Ts).tolist(),
618
+ "2xCO2_temperature_profile": np.squeeze(model_2xCO2.Ts).tolist(),
619
+ "latitudes": model_ctrl.lat.tolist(),
620
+ }
621
+ return {"success": True, "result": result, "error": None}
622
  except Exception as e:
623
+ return {"success": False, "result": None, "error": str(e)}
624
+
625
 
626
+ # ==================== Physical Constants Tools ====================
627
+
628
+ @mcp.tool(name="get_climate_constants", description="Get commonly used climate physical constants")
629
+ def get_climate_constants_tool() -> dict:
630
  """
631
+ Get commonly used climate physical constants from climlab.
632
 
633
+ :return: A dictionary containing physical constants.
 
634
  """
635
  try:
636
+ result = {
637
+ "earth_radius_m": const.a,
638
+ "gravitational_acceleration_m_s2": const.g,
639
+ "solar_constant_W_m2": const.S0,
640
+ "stefan_boltzmann_constant_W_m2_K4": const.sigma,
641
+ "specific_heat_dry_air_J_kg_K": const.cp,
642
+ "gas_constant_dry_air_J_kg_K": const.Rd,
643
+ "latent_heat_vaporization_J_kg": const.Lhvap,
644
+ "latent_heat_fusion_J_kg": const.Lhfus,
645
+ "water_density_kg_m3": const.rho_w,
646
+ "specific_heat_water_J_kg_K": const.cw,
647
+ "seconds_per_day": const.seconds_per_day,
648
+ "days_per_year": const.days_per_year,
649
+ "earth_surface_area_m2": const.area_earth,
650
+ "present_day_orbital_parameters": const.orb_present,
651
  }
652
+ return {"success": True, "result": result, "error": None}
653
  except Exception as e:
654
+ return {"success": False, "result": None, "error": str(e)}
655
+
656
+
657
+ # ==================== Utility Tools ====================
658
 
659
+ @mcp.tool(name="calculate_global_mean", description="Calculate area-weighted global mean of a latitude-dependent field")
660
+ def calculate_global_mean_tool(values: list, latitudes: list = None) -> dict:
661
  """
662
+ Calculate the global mean of a field with proper area weighting.
663
 
664
+ :param values: List of field values at each latitude.
665
+ :param latitudes: List of latitudes in degrees (optional, defaults to even spacing).
666
+ :return: A dictionary containing the global mean.
667
  """
668
  try:
669
+ values_array = np.array(values)
670
+ if latitudes is None:
671
+ latitudes = np.linspace(-90, 90, len(values_array))
672
+ lat_array = np.array(latitudes)
673
+
674
+ # Area weighting using cosine of latitude
675
+ weights = np.cos(np.deg2rad(lat_array))
676
+ global_mean_value = float(np.average(values_array, weights=weights))
677
+
678
+ result = {
679
+ "global_mean": global_mean_value,
680
+ "simple_mean": float(np.mean(values_array)),
681
+ "max_value": float(np.max(values_array)),
682
+ "min_value": float(np.min(values_array)),
683
+ "num_points": len(values_array),
684
  }
685
+ return {"success": True, "result": result, "error": None}
686
  except Exception as e:
687
+ return {"success": False, "result": None, "error": str(e)}
688
 
689
+
690
+ @mcp.tool(name="calculate_meridional_heat_transport", description="Calculate implied meridional heat transport from energy imbalance")
691
+ def calculate_meridional_heat_transport_tool(ASR: list, OLR: list, latitudes: list = None) -> dict:
692
  """
693
+ Calculate meridional heat transport from absorbed solar radiation and OLR.
694
 
695
+ :param ASR: List of absorbed shortwave radiation values (W/m²).
696
+ :param OLR: List of outgoing longwave radiation values (W/m²).
697
+ :param latitudes: List of latitudes in degrees (optional).
698
+ :return: A dictionary containing the heat transport profile.
699
  """
700
  try:
701
+ ASR_arr = np.array(ASR)
702
+ OLR_arr = np.array(OLR)
703
+
704
+ if latitudes is None:
705
+ latitudes = np.linspace(-90, 90, len(ASR_arr))
706
+ lat = np.array(latitudes)
707
+
708
+ # Net radiation at TOA
709
+ net_rad = ASR_arr - OLR_arr
710
+
711
+ # Calculate heat transport by integrating from South Pole
712
+ phi = np.deg2rad(lat)
713
+ dlat = np.abs(lat[1] - lat[0]) if len(lat) > 1 else 1.0
714
+ dphi = np.deg2rad(dlat)
715
+
716
+ # Integrate: H(φ) = 2πa² ∫ R(φ') cos(φ') dφ'
717
+ integrand = net_rad * np.cos(phi)
718
+ heat_transport = 2 * np.pi * const.a**2 * np.cumsum(integrand) * dphi
719
+ heat_transport_PW = heat_transport * 1e-15 # Convert to PW
720
+
721
+ result = {
722
+ "latitudes": lat.tolist(),
723
+ "net_radiation_W_m2": net_rad.tolist(),
724
+ "heat_transport_PW": heat_transport_PW.tolist(),
725
+ "max_poleward_transport_PW": float(np.max(np.abs(heat_transport_PW))),
726
+ "latitude_of_max_transport": float(lat[np.argmax(np.abs(heat_transport_PW))]),
727
  }
728
+ return {"success": True, "result": result, "error": None}
729
  except Exception as e:
730
+ return {"success": False, "result": None, "error": str(e)}
731
+
732
 
 
733
  def create_app() -> FastMCP:
734
  """
735
+ Create and return the FastMCP application instance.
736
 
737
+ :return: The FastMCP application instance.
 
738
  """
739
  return mcp