paraview-mcp-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paraview_mcp_python-0.1.0.dist-info/METADATA +339 -0
- paraview_mcp_python-0.1.0.dist-info/RECORD +8 -0
- paraview_mcp_python-0.1.0.dist-info/WHEEL +4 -0
- paraview_mcp_python-0.1.0.dist-info/entry_points.txt +2 -0
- paraview_mcp_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- paraview_mcp_server/__init__.py +4 -0
- paraview_mcp_server/headless.py +350 -0
- paraview_mcp_server/server.py +671 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"""ParaView MCP Server — External MCP server that bridges AI assistants to ParaView."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from contextlib import asynccontextmanager, suppress
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import Context, FastMCP
|
|
11
|
+
|
|
12
|
+
from paraview_mcp_server.headless import HeadlessJobManager, HeadlessPvpythonExecutor
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
PARAVIEW_HOST = "127.0.0.1"
|
|
17
|
+
PARAVIEW_PORT = 9876
|
|
18
|
+
HEADLESS_JOB_MANAGER = HeadlessJobManager()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParaViewConnection:
|
|
22
|
+
"""Async TCP client that communicates with the ParaView bridge server."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, host: str = PARAVIEW_HOST, port: int = PARAVIEW_PORT):
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = port
|
|
27
|
+
self._reader: asyncio.StreamReader | None = None
|
|
28
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
29
|
+
self._lock = asyncio.Lock()
|
|
30
|
+
|
|
31
|
+
async def connect(self):
|
|
32
|
+
self._reader, self._writer = await asyncio.open_connection(self.host, self.port)
|
|
33
|
+
logger.info("Connected to ParaView bridge at %s:%s", self.host, self.port)
|
|
34
|
+
|
|
35
|
+
async def _drop_connection(self):
|
|
36
|
+
writer = self._writer
|
|
37
|
+
self._reader = None
|
|
38
|
+
self._writer = None
|
|
39
|
+
if writer is not None:
|
|
40
|
+
writer.close()
|
|
41
|
+
with suppress(OSError):
|
|
42
|
+
await writer.wait_closed()
|
|
43
|
+
|
|
44
|
+
async def disconnect(self):
|
|
45
|
+
await self._drop_connection()
|
|
46
|
+
|
|
47
|
+
async def send_command(self, command: str, params: dict | None = None) -> Any:
|
|
48
|
+
"""Send a command to the ParaView bridge and return the result."""
|
|
49
|
+
if not self._writer:
|
|
50
|
+
await self.connect()
|
|
51
|
+
|
|
52
|
+
assert self._reader is not None
|
|
53
|
+
assert self._writer is not None
|
|
54
|
+
request_id = str(uuid.uuid4())
|
|
55
|
+
|
|
56
|
+
request = {
|
|
57
|
+
"id": request_id,
|
|
58
|
+
"command": command,
|
|
59
|
+
"params": params or {},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async with self._lock:
|
|
63
|
+
try:
|
|
64
|
+
self._writer.write(json.dumps(request).encode() + b"\n")
|
|
65
|
+
await self._writer.drain()
|
|
66
|
+
|
|
67
|
+
line = await self._reader.readline()
|
|
68
|
+
if not line:
|
|
69
|
+
raise ConnectionError("ParaView bridge connection closed")
|
|
70
|
+
|
|
71
|
+
response = json.loads(line)
|
|
72
|
+
if response.get("id") != request_id:
|
|
73
|
+
raise ConnectionError(
|
|
74
|
+
f"ParaView bridge sent mismatched response id {response.get('id')!r} for request {request_id!r}"
|
|
75
|
+
)
|
|
76
|
+
if not response.get("success"):
|
|
77
|
+
raise RuntimeError(response.get("error", "Unknown error from ParaView bridge"))
|
|
78
|
+
return response.get("result")
|
|
79
|
+
except asyncio.CancelledError:
|
|
80
|
+
await self._drop_connection()
|
|
81
|
+
raise
|
|
82
|
+
except (ConnectionError, OSError, json.JSONDecodeError) as e:
|
|
83
|
+
await self._drop_connection()
|
|
84
|
+
if isinstance(e, json.JSONDecodeError):
|
|
85
|
+
raise ConnectionError("Lost connection to ParaView bridge: invalid JSON response") from e
|
|
86
|
+
raise ConnectionError(f"Lost connection to ParaView bridge: {e}") from e
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@asynccontextmanager
|
|
90
|
+
async def paraview_lifespan(server: FastMCP):
|
|
91
|
+
"""Manage the ParaView bridge connection lifecycle."""
|
|
92
|
+
conn = ParaViewConnection()
|
|
93
|
+
try:
|
|
94
|
+
await conn.connect()
|
|
95
|
+
except OSError:
|
|
96
|
+
logger.warning("Could not connect to ParaView bridge on startup. Will retry on first tool call.")
|
|
97
|
+
yield conn
|
|
98
|
+
await conn.disconnect()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
mcp = FastMCP(
|
|
102
|
+
"ParaView MCP Server",
|
|
103
|
+
lifespan=paraview_lifespan,
|
|
104
|
+
log_level="INFO",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_conn(ctx: Context) -> ParaViewConnection:
|
|
109
|
+
return ctx.request_context.lifespan_context # type: ignore[no-any-return]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ======================================================================
|
|
113
|
+
# Scene / session tools
|
|
114
|
+
# ======================================================================
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp.tool(
|
|
118
|
+
name="paraview_scene_get_info",
|
|
119
|
+
description="Get basic information about the current ParaView session, including source count and active view type.",
|
|
120
|
+
)
|
|
121
|
+
async def scene_get_info(ctx: Context) -> str:
|
|
122
|
+
result = await _get_conn(ctx).send_command("scene.get_info")
|
|
123
|
+
return json.dumps(result, indent=2)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool(
|
|
127
|
+
name="paraview_scene_list_sources",
|
|
128
|
+
description="List all sources currently loaded in the ParaView pipeline browser.",
|
|
129
|
+
)
|
|
130
|
+
async def scene_list_sources(ctx: Context) -> str:
|
|
131
|
+
result = await _get_conn(ctx).send_command("scene.list_sources")
|
|
132
|
+
return json.dumps(result, indent=2)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool(
|
|
136
|
+
name="paraview_scene_list_views",
|
|
137
|
+
description="List all open views/render windows in the current ParaView session.",
|
|
138
|
+
)
|
|
139
|
+
async def scene_list_views(ctx: Context) -> str:
|
|
140
|
+
result = await _get_conn(ctx).send_command("scene.list_views")
|
|
141
|
+
return json.dumps(result, indent=2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool(
|
|
145
|
+
name="paraview_source_get_properties",
|
|
146
|
+
description="Get the properties and metadata of a named source in the ParaView pipeline.",
|
|
147
|
+
)
|
|
148
|
+
async def source_get_properties(ctx: Context, name: str) -> str:
|
|
149
|
+
result = await _get_conn(ctx).send_command("source.get_properties", {"name": name})
|
|
150
|
+
return json.dumps(result, indent=2)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ======================================================================
|
|
154
|
+
# Data loading tools
|
|
155
|
+
# ======================================================================
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@mcp.tool(
|
|
159
|
+
name="paraview_source_open_file",
|
|
160
|
+
description="Open a supported dataset file (VTK, VTU, VTS, ExodusII, CSV, etc.) in ParaView.",
|
|
161
|
+
)
|
|
162
|
+
async def source_open_file(ctx: Context, filepath: str) -> str:
|
|
163
|
+
result = await _get_conn(ctx).send_command("source.open_file", {"filepath": filepath})
|
|
164
|
+
return json.dumps(result, indent=2)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@mcp.tool(
|
|
168
|
+
name="paraview_source_delete",
|
|
169
|
+
description="Delete a named source from the ParaView pipeline.",
|
|
170
|
+
)
|
|
171
|
+
async def source_delete(ctx: Context, name: str) -> str:
|
|
172
|
+
result = await _get_conn(ctx).send_command("source.delete", {"name": name})
|
|
173
|
+
return json.dumps(result, indent=2)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp.tool(
|
|
177
|
+
name="paraview_source_rename",
|
|
178
|
+
description="Rename a source in the ParaView pipeline.",
|
|
179
|
+
)
|
|
180
|
+
async def source_rename(ctx: Context, name: str, new_name: str) -> str:
|
|
181
|
+
result = await _get_conn(ctx).send_command("source.rename", {"name": name, "new_name": new_name})
|
|
182
|
+
return json.dumps(result, indent=2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ======================================================================
|
|
186
|
+
# Filter tools — basic
|
|
187
|
+
# ======================================================================
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool(
|
|
191
|
+
name="paraview_filter_slice",
|
|
192
|
+
description=(
|
|
193
|
+
"Apply a Slice filter to a named source. "
|
|
194
|
+
"Specify origin as [x, y, z] and normal as [nx, ny, nz]. "
|
|
195
|
+
"Defaults: origin=[0,0,0], normal=[1,0,0]."
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
async def filter_slice(
|
|
199
|
+
ctx: Context,
|
|
200
|
+
input: str,
|
|
201
|
+
origin: list[float] | None = None,
|
|
202
|
+
normal: list[float] | None = None,
|
|
203
|
+
) -> str:
|
|
204
|
+
params: dict[str, Any] = {"input": input}
|
|
205
|
+
if origin is not None:
|
|
206
|
+
params["origin"] = origin
|
|
207
|
+
if normal is not None:
|
|
208
|
+
params["normal"] = normal
|
|
209
|
+
result = await _get_conn(ctx).send_command("filter.slice", params)
|
|
210
|
+
return json.dumps(result, indent=2)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@mcp.tool(
|
|
214
|
+
name="paraview_filter_clip",
|
|
215
|
+
description=(
|
|
216
|
+
"Apply a Clip filter to a named source. "
|
|
217
|
+
"Specify origin as [x, y, z] and normal as [nx, ny, nz]. "
|
|
218
|
+
"Defaults: origin=[0,0,0], normal=[1,0,0]."
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
async def filter_clip(
|
|
222
|
+
ctx: Context,
|
|
223
|
+
input: str,
|
|
224
|
+
origin: list[float] | None = None,
|
|
225
|
+
normal: list[float] | None = None,
|
|
226
|
+
) -> str:
|
|
227
|
+
params: dict[str, Any] = {"input": input}
|
|
228
|
+
if origin is not None:
|
|
229
|
+
params["origin"] = origin
|
|
230
|
+
if normal is not None:
|
|
231
|
+
params["normal"] = normal
|
|
232
|
+
result = await _get_conn(ctx).send_command("filter.clip", params)
|
|
233
|
+
return json.dumps(result, indent=2)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@mcp.tool(
|
|
237
|
+
name="paraview_filter_contour",
|
|
238
|
+
description=(
|
|
239
|
+
"Apply a Contour (isosurface) filter to a named source. "
|
|
240
|
+
"Specify the scalar array name and one or more isovalues."
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
async def filter_contour(
|
|
244
|
+
ctx: Context,
|
|
245
|
+
input: str,
|
|
246
|
+
array: str,
|
|
247
|
+
values: list[float],
|
|
248
|
+
) -> str:
|
|
249
|
+
result = await _get_conn(ctx).send_command("filter.contour", {"input": input, "array": array, "values": values})
|
|
250
|
+
return json.dumps(result, indent=2)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@mcp.tool(
|
|
254
|
+
name="paraview_filter_threshold",
|
|
255
|
+
description=(
|
|
256
|
+
"Apply a Threshold filter to a named source. Keep cells where the scalar array falls within [lower, upper]."
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
async def filter_threshold(
|
|
260
|
+
ctx: Context,
|
|
261
|
+
input: str,
|
|
262
|
+
array: str,
|
|
263
|
+
lower: float,
|
|
264
|
+
upper: float,
|
|
265
|
+
) -> str:
|
|
266
|
+
result = await _get_conn(ctx).send_command(
|
|
267
|
+
"filter.threshold",
|
|
268
|
+
{"input": input, "array": array, "lower": lower, "upper": upper},
|
|
269
|
+
)
|
|
270
|
+
return json.dumps(result, indent=2)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ======================================================================
|
|
274
|
+
# Filter tools — advanced
|
|
275
|
+
# ======================================================================
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@mcp.tool(
|
|
279
|
+
name="paraview_filter_calculator",
|
|
280
|
+
description=(
|
|
281
|
+
"Apply a Calculator filter to a named source. "
|
|
282
|
+
"Provide a mathematical expression (e.g. 'Pressure * 2') and an "
|
|
283
|
+
"optional result array name (default: 'Result')."
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
async def filter_calculator(
|
|
287
|
+
ctx: Context,
|
|
288
|
+
input: str,
|
|
289
|
+
expression: str,
|
|
290
|
+
result_name: str = "Result",
|
|
291
|
+
attribute_type: str = "Point Data",
|
|
292
|
+
) -> str:
|
|
293
|
+
result = await _get_conn(ctx).send_command(
|
|
294
|
+
"filter.calculator",
|
|
295
|
+
{
|
|
296
|
+
"input": input,
|
|
297
|
+
"expression": expression,
|
|
298
|
+
"result_name": result_name,
|
|
299
|
+
"attribute_type": attribute_type,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
return json.dumps(result, indent=2)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@mcp.tool(
|
|
306
|
+
name="paraview_filter_stream_tracer",
|
|
307
|
+
description=("Apply a Stream Tracer filter to a named vector source. Generates streamlines from seed points."),
|
|
308
|
+
)
|
|
309
|
+
async def filter_stream_tracer(
|
|
310
|
+
ctx: Context,
|
|
311
|
+
input: str,
|
|
312
|
+
seed_type: str = "Point Cloud",
|
|
313
|
+
integration_direction: str = "BOTH",
|
|
314
|
+
num_points: int = 100,
|
|
315
|
+
max_length: float = 1.0,
|
|
316
|
+
) -> str:
|
|
317
|
+
result = await _get_conn(ctx).send_command(
|
|
318
|
+
"filter.stream_tracer",
|
|
319
|
+
{
|
|
320
|
+
"input": input,
|
|
321
|
+
"seed_type": seed_type,
|
|
322
|
+
"integration_direction": integration_direction,
|
|
323
|
+
"num_points": num_points,
|
|
324
|
+
"max_length": max_length,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
return json.dumps(result, indent=2)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@mcp.tool(
|
|
331
|
+
name="paraview_filter_glyph",
|
|
332
|
+
description=("Apply a Glyph filter to a named source. Glyphs visualize vector data using arrows, spheres, etc."),
|
|
333
|
+
)
|
|
334
|
+
async def filter_glyph(
|
|
335
|
+
ctx: Context,
|
|
336
|
+
input: str,
|
|
337
|
+
glyph_type: str = "Arrow",
|
|
338
|
+
scale_array: str | None = None,
|
|
339
|
+
scale_factor: float = 1.0,
|
|
340
|
+
) -> str:
|
|
341
|
+
params: dict[str, Any] = {
|
|
342
|
+
"input": input,
|
|
343
|
+
"glyph_type": glyph_type,
|
|
344
|
+
"scale_factor": scale_factor,
|
|
345
|
+
}
|
|
346
|
+
if scale_array is not None:
|
|
347
|
+
params["scale_array"] = scale_array
|
|
348
|
+
result = await _get_conn(ctx).send_command("filter.glyph", params)
|
|
349
|
+
return json.dumps(result, indent=2)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ======================================================================
|
|
353
|
+
# Display / coloring tools
|
|
354
|
+
# ======================================================================
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@mcp.tool(
|
|
358
|
+
name="paraview_display_show",
|
|
359
|
+
description="Make a named source visible in the active ParaView render view.",
|
|
360
|
+
)
|
|
361
|
+
async def display_show(ctx: Context, name: str) -> str:
|
|
362
|
+
result = await _get_conn(ctx).send_command("display.show", {"name": name})
|
|
363
|
+
return json.dumps(result, indent=2)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool(
|
|
367
|
+
name="paraview_display_hide",
|
|
368
|
+
description="Hide a named source in the active ParaView render view.",
|
|
369
|
+
)
|
|
370
|
+
async def display_hide(ctx: Context, name: str) -> str:
|
|
371
|
+
result = await _get_conn(ctx).send_command("display.hide", {"name": name})
|
|
372
|
+
return json.dumps(result, indent=2)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@mcp.tool(
|
|
376
|
+
name="paraview_display_color_by",
|
|
377
|
+
description=(
|
|
378
|
+
"Color a named source by a data array. "
|
|
379
|
+
"Specify the array name and optionally the component index "
|
|
380
|
+
"(-1 = magnitude, 0/1/2 = X/Y/Z). "
|
|
381
|
+
"association can be 'POINTS' (default) or 'CELLS'."
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
async def display_color_by(
|
|
385
|
+
ctx: Context,
|
|
386
|
+
name: str,
|
|
387
|
+
array: str,
|
|
388
|
+
component: int = -1,
|
|
389
|
+
association: str = "POINTS",
|
|
390
|
+
) -> str:
|
|
391
|
+
result = await _get_conn(ctx).send_command(
|
|
392
|
+
"display.color_by",
|
|
393
|
+
{"name": name, "array": array, "component": component, "association": association},
|
|
394
|
+
)
|
|
395
|
+
return json.dumps(result, indent=2)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@mcp.tool(
|
|
399
|
+
name="paraview_display_set_representation",
|
|
400
|
+
description=(
|
|
401
|
+
"Set the display representation for a named source. "
|
|
402
|
+
"Supported types: Surface, Wireframe, Points, Surface With Edges, Volume."
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
async def display_set_representation(ctx: Context, name: str, representation: str) -> str:
|
|
406
|
+
result = await _get_conn(ctx).send_command(
|
|
407
|
+
"display.set_representation",
|
|
408
|
+
{"name": name, "representation": representation},
|
|
409
|
+
)
|
|
410
|
+
return json.dumps(result, indent=2)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@mcp.tool(
|
|
414
|
+
name="paraview_display_set_opacity",
|
|
415
|
+
description="Set the opacity (0.0 = fully transparent, 1.0 = fully opaque) of a named source.",
|
|
416
|
+
)
|
|
417
|
+
async def display_set_opacity(ctx: Context, name: str, opacity: float) -> str:
|
|
418
|
+
result = await _get_conn(ctx).send_command("display.set_opacity", {"name": name, "opacity": opacity})
|
|
419
|
+
return json.dumps(result, indent=2)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@mcp.tool(
|
|
423
|
+
name="paraview_display_rescale_transfer_function",
|
|
424
|
+
description="Rescale the color transfer function of a named source to fit the current data range.",
|
|
425
|
+
)
|
|
426
|
+
async def display_rescale_transfer_function(ctx: Context, name: str) -> str:
|
|
427
|
+
result = await _get_conn(ctx).send_command("display.rescale_transfer_function", {"name": name})
|
|
428
|
+
return json.dumps(result, indent=2)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ======================================================================
|
|
432
|
+
# View / camera tools
|
|
433
|
+
# ======================================================================
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@mcp.tool(
|
|
437
|
+
name="paraview_view_reset_camera",
|
|
438
|
+
description="Reset the camera in the active ParaView render view to fit all visible sources.",
|
|
439
|
+
)
|
|
440
|
+
async def view_reset_camera(ctx: Context) -> str:
|
|
441
|
+
result = await _get_conn(ctx).send_command("view.reset_camera")
|
|
442
|
+
return json.dumps(result, indent=2)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@mcp.tool(
|
|
446
|
+
name="paraview_view_set_camera",
|
|
447
|
+
description=(
|
|
448
|
+
"Set the camera position, focal point, and view-up vector. "
|
|
449
|
+
"Each parameter is a [x, y, z] list. Optionally set parallel_scale for orthographic views."
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
async def view_set_camera(
|
|
453
|
+
ctx: Context,
|
|
454
|
+
position: list[float] | None = None,
|
|
455
|
+
focal_point: list[float] | None = None,
|
|
456
|
+
view_up: list[float] | None = None,
|
|
457
|
+
parallel_scale: float | None = None,
|
|
458
|
+
) -> str:
|
|
459
|
+
params: dict[str, Any] = {}
|
|
460
|
+
if position is not None:
|
|
461
|
+
params["position"] = position
|
|
462
|
+
if focal_point is not None:
|
|
463
|
+
params["focal_point"] = focal_point
|
|
464
|
+
if view_up is not None:
|
|
465
|
+
params["view_up"] = view_up
|
|
466
|
+
if parallel_scale is not None:
|
|
467
|
+
params["parallel_scale"] = parallel_scale
|
|
468
|
+
result = await _get_conn(ctx).send_command("view.set_camera", params)
|
|
469
|
+
return json.dumps(result, indent=2)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@mcp.tool(
|
|
473
|
+
name="paraview_view_set_background",
|
|
474
|
+
description=(
|
|
475
|
+
"Set the background color of the active render view. "
|
|
476
|
+
"Provide color as [r, g, b] with values 0-1. "
|
|
477
|
+
"Optionally provide color2 for a gradient background."
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
async def view_set_background(
|
|
481
|
+
ctx: Context,
|
|
482
|
+
color: list[float],
|
|
483
|
+
color2: list[float] | None = None,
|
|
484
|
+
) -> str:
|
|
485
|
+
params: dict[str, Any] = {"color": color}
|
|
486
|
+
if color2 is not None:
|
|
487
|
+
params["color2"] = color2
|
|
488
|
+
result = await _get_conn(ctx).send_command("view.set_background", params)
|
|
489
|
+
return json.dumps(result, indent=2)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ======================================================================
|
|
493
|
+
# Export tools
|
|
494
|
+
# ======================================================================
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@mcp.tool(
|
|
498
|
+
name="paraview_export_screenshot",
|
|
499
|
+
description=(
|
|
500
|
+
"Save a screenshot of the active ParaView render view to a file. "
|
|
501
|
+
"Supports PNG and JPEG. Default resolution is 1920×1080."
|
|
502
|
+
),
|
|
503
|
+
)
|
|
504
|
+
async def export_screenshot(
|
|
505
|
+
ctx: Context,
|
|
506
|
+
filepath: str,
|
|
507
|
+
width: int = 1920,
|
|
508
|
+
height: int = 1080,
|
|
509
|
+
transparent: bool = False,
|
|
510
|
+
) -> str:
|
|
511
|
+
result = await _get_conn(ctx).send_command(
|
|
512
|
+
"export.screenshot",
|
|
513
|
+
{"filepath": filepath, "width": width, "height": height, "transparent": transparent},
|
|
514
|
+
)
|
|
515
|
+
return json.dumps(result, indent=2)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@mcp.tool(
|
|
519
|
+
name="paraview_export_data",
|
|
520
|
+
description=(
|
|
521
|
+
"Export a named source's data to a file. "
|
|
522
|
+
"The output format is determined by the file extension (e.g. .vtu, .csv, .vtk)."
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
async def export_data(ctx: Context, name: str, filepath: str) -> str:
|
|
526
|
+
result = await _get_conn(ctx).send_command("export.data", {"name": name, "filepath": filepath})
|
|
527
|
+
return json.dumps(result, indent=2)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@mcp.tool(
|
|
531
|
+
name="paraview_export_animation",
|
|
532
|
+
description=(
|
|
533
|
+
"Export an animation of the current ParaView scene. "
|
|
534
|
+
"The output format is determined by the file extension (e.g. .avi, .ogv, .png for frame series). "
|
|
535
|
+
"Default resolution is 1920×1080, frame rate 15 fps."
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
async def export_animation(
|
|
539
|
+
ctx: Context,
|
|
540
|
+
filepath: str,
|
|
541
|
+
width: int = 1920,
|
|
542
|
+
height: int = 1080,
|
|
543
|
+
frame_rate: int = 15,
|
|
544
|
+
frame_start: int | None = None,
|
|
545
|
+
frame_end: int | None = None,
|
|
546
|
+
) -> str:
|
|
547
|
+
params: dict[str, Any] = {
|
|
548
|
+
"filepath": filepath,
|
|
549
|
+
"width": width,
|
|
550
|
+
"height": height,
|
|
551
|
+
"frame_rate": frame_rate,
|
|
552
|
+
}
|
|
553
|
+
if frame_start is not None:
|
|
554
|
+
params["frame_start"] = frame_start
|
|
555
|
+
if frame_end is not None:
|
|
556
|
+
params["frame_end"] = frame_end
|
|
557
|
+
result = await _get_conn(ctx).send_command("export.animation", params)
|
|
558
|
+
return json.dumps(result, indent=2)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ======================================================================
|
|
562
|
+
# Python execution tools
|
|
563
|
+
# ======================================================================
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@mcp.tool(
|
|
567
|
+
name="paraview_python_exec",
|
|
568
|
+
description=(
|
|
569
|
+
"Execute a Python script in the ParaView bridge context synchronously. "
|
|
570
|
+
"Provide either 'code' (inline Python string) or 'script_path' (path to a .py file), not both. "
|
|
571
|
+
"The script has access to 'paraview.simple' as 'pvs' and an 'args' dict "
|
|
572
|
+
"with any supplied arguments. Set '__result__' to return a JSON-serializable value. "
|
|
573
|
+
"Returns result, stdout, stderr, error, and execution duration. "
|
|
574
|
+
"Use transport='bridge' for the live bridge session, or transport='headless' "
|
|
575
|
+
"to run in a separate pvpython process."
|
|
576
|
+
),
|
|
577
|
+
)
|
|
578
|
+
async def python_exec(
|
|
579
|
+
ctx: Context,
|
|
580
|
+
code: str | None = None,
|
|
581
|
+
script_path: str | None = None,
|
|
582
|
+
args: dict | None = None,
|
|
583
|
+
timeout_seconds: int | None = None,
|
|
584
|
+
transport: str = "bridge",
|
|
585
|
+
) -> str:
|
|
586
|
+
if transport == "headless":
|
|
587
|
+
executor = HeadlessPvpythonExecutor()
|
|
588
|
+
result = await executor.execute(
|
|
589
|
+
code=code,
|
|
590
|
+
script_path=script_path,
|
|
591
|
+
args=args,
|
|
592
|
+
timeout_seconds=timeout_seconds,
|
|
593
|
+
)
|
|
594
|
+
else:
|
|
595
|
+
params: dict[str, Any] = {}
|
|
596
|
+
if code is not None:
|
|
597
|
+
params["code"] = code
|
|
598
|
+
if script_path is not None:
|
|
599
|
+
params["script_path"] = script_path
|
|
600
|
+
if args is not None:
|
|
601
|
+
params["args"] = args
|
|
602
|
+
if timeout_seconds is not None:
|
|
603
|
+
params["timeout_seconds"] = timeout_seconds
|
|
604
|
+
result = await _get_conn(ctx).send_command("python.execute", params)
|
|
605
|
+
return json.dumps(result, indent=2)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@mcp.tool(
|
|
609
|
+
name="paraview_python_exec_async",
|
|
610
|
+
description=(
|
|
611
|
+
"Start a long-running Python script in ParaView asynchronously. "
|
|
612
|
+
"Same parameters as paraview_python_exec. Returns a job_id immediately. "
|
|
613
|
+
"Use paraview_job_status to poll for completion, and paraview_job_cancel to abort. "
|
|
614
|
+
"Uses transport='headless' to run in a separate pvpython process."
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
async def python_exec_async(
|
|
618
|
+
ctx: Context,
|
|
619
|
+
code: str | None = None,
|
|
620
|
+
script_path: str | None = None,
|
|
621
|
+
args: dict | None = None,
|
|
622
|
+
timeout_seconds: int | None = None,
|
|
623
|
+
) -> str:
|
|
624
|
+
executor = HeadlessPvpythonExecutor()
|
|
625
|
+
job_id = await HEADLESS_JOB_MANAGER.create_job(
|
|
626
|
+
executor,
|
|
627
|
+
code=code,
|
|
628
|
+
script_path=script_path,
|
|
629
|
+
args=args,
|
|
630
|
+
timeout_seconds=timeout_seconds,
|
|
631
|
+
)
|
|
632
|
+
return json.dumps({"job_id": job_id}, indent=2)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@mcp.tool(
|
|
636
|
+
name="paraview_job_status",
|
|
637
|
+
description=(
|
|
638
|
+
"Get the status of an async ParaView job. Returns job_id, status "
|
|
639
|
+
"(queued/running/succeeded/failed/cancelled), timestamps, result, stdout, stderr, and error. "
|
|
640
|
+
"Poll this after starting a job with paraview_python_exec_async."
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
async def job_status(ctx: Context, job_id: str) -> str:
|
|
644
|
+
result = HEADLESS_JOB_MANAGER.get_status(job_id)
|
|
645
|
+
return json.dumps(result, indent=2)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@mcp.tool(
|
|
649
|
+
name="paraview_job_cancel",
|
|
650
|
+
description=("Cancel a running or queued async ParaView job."),
|
|
651
|
+
)
|
|
652
|
+
async def job_cancel(ctx: Context, job_id: str) -> str:
|
|
653
|
+
result = await HEADLESS_JOB_MANAGER.cancel(job_id)
|
|
654
|
+
return json.dumps(result, indent=2)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@mcp.tool(
|
|
658
|
+
name="paraview_job_list",
|
|
659
|
+
description="List known async ParaView jobs with their IDs, statuses, and creation timestamps.",
|
|
660
|
+
)
|
|
661
|
+
async def job_list(ctx: Context) -> str:
|
|
662
|
+
result = HEADLESS_JOB_MANAGER.list_jobs()
|
|
663
|
+
return json.dumps(result, indent=2)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def main():
|
|
667
|
+
mcp.run(transport="stdio")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
if __name__ == "__main__":
|
|
671
|
+
main()
|