holoviz-mcp 0.0.1a0__py3-none-any.whl → 0.0.1a2__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.

Potentially problematic release.


This version of holoviz-mcp might be problematic. Click here for more details.

Files changed (37) hide show
  1. holoviz_mcp/__init__.py +18 -0
  2. holoviz_mcp/apps/__init__.py +1 -0
  3. holoviz_mcp/apps/configuration_viewer.py +116 -0
  4. holoviz_mcp/apps/search.py +314 -0
  5. holoviz_mcp/config/__init__.py +31 -0
  6. holoviz_mcp/config/config.yaml +167 -0
  7. holoviz_mcp/config/loader.py +308 -0
  8. holoviz_mcp/config/models.py +216 -0
  9. holoviz_mcp/config/resources/best-practices/hvplot.md +62 -0
  10. holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
  11. holoviz_mcp/config/resources/best-practices/panel.md +294 -0
  12. holoviz_mcp/config/schema.json +203 -0
  13. holoviz_mcp/docs_mcp/__init__.py +1 -0
  14. holoviz_mcp/docs_mcp/data.py +963 -0
  15. holoviz_mcp/docs_mcp/models.py +21 -0
  16. holoviz_mcp/docs_mcp/pages_design.md +407 -0
  17. holoviz_mcp/docs_mcp/server.py +220 -0
  18. holoviz_mcp/hvplot_mcp/__init__.py +1 -0
  19. holoviz_mcp/hvplot_mcp/server.py +152 -0
  20. holoviz_mcp/panel_mcp/__init__.py +17 -0
  21. holoviz_mcp/panel_mcp/data.py +316 -0
  22. holoviz_mcp/panel_mcp/models.py +124 -0
  23. holoviz_mcp/panel_mcp/server.py +650 -0
  24. holoviz_mcp/py.typed +0 -0
  25. holoviz_mcp/serve.py +34 -0
  26. holoviz_mcp/server.py +77 -0
  27. holoviz_mcp/shared/__init__.py +1 -0
  28. holoviz_mcp/shared/extract_tools.py +74 -0
  29. holoviz_mcp-0.0.1a2.dist-info/METADATA +641 -0
  30. holoviz_mcp-0.0.1a2.dist-info/RECORD +33 -0
  31. {holoviz_mcp-0.0.1a0.dist-info → holoviz_mcp-0.0.1a2.dist-info}/WHEEL +1 -2
  32. holoviz_mcp-0.0.1a2.dist-info/entry_points.txt +4 -0
  33. holoviz_mcp-0.0.1a2.dist-info/licenses/LICENSE.txt +30 -0
  34. holoviz_mcp-0.0.1a0.dist-info/METADATA +0 -6
  35. holoviz_mcp-0.0.1a0.dist-info/RECORD +0 -5
  36. holoviz_mcp-0.0.1a0.dist-info/top_level.txt +0 -1
  37. main.py +0 -6
@@ -0,0 +1,650 @@
1
+ """[Panel](https://panel.holoviz.org/) MCP Server.
2
+
3
+ This MCP server provides tools, resources and prompts for using Panel to develop quick, interactive
4
+ applications, tools and dashboards in Python using best practices.
5
+
6
+ Use this server to access:
7
+
8
+ - Panel Best Practices: Learn how to use Panel effectively.
9
+ - Panel Components: Get information about specific Panel components like widgets (input), panes (output) and layouts.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import subprocess
15
+ import threading
16
+ from importlib.metadata import distributions
17
+ from typing import Optional
18
+ from typing import cast
19
+
20
+ from fastmcp import Context
21
+ from fastmcp import FastMCP
22
+
23
+ from holoviz_mcp.config.loader import get_config
24
+ from holoviz_mcp.panel_mcp.data import get_components as _get_components_org
25
+ from holoviz_mcp.panel_mcp.data import to_proxy_url
26
+ from holoviz_mcp.panel_mcp.models import ComponentDetails
27
+ from holoviz_mcp.panel_mcp.models import ComponentSummary
28
+ from holoviz_mcp.panel_mcp.models import ComponentSummarySearchResult
29
+ from holoviz_mcp.panel_mcp.models import ParameterInfo
30
+
31
+ # Create the FastMCP server
32
+ mcp = FastMCP(
33
+ name="panel",
34
+ instructions="""
35
+ [Panel](https://panel.holoviz.org/) MCP Server.
36
+
37
+ This MCP server provides tools, resources and prompts for using Panel to develop quick, interactive
38
+ applications, tools and dashboards in Python using best practices.
39
+
40
+ DO use this server to search for specific Panel components and access detailed information including docstrings and parameter information.
41
+ """,
42
+ )
43
+
44
+ _config = get_config()
45
+
46
+
47
+ async def _list_packages_depending_on(target_package: str, ctx: Context) -> list[str]:
48
+ """
49
+ Find all installed packages that depend on a given package.
50
+
51
+ This is a helper function that searches through installed packages to find
52
+ those that have the target package as a dependency. Used to discover
53
+ Panel-related packages in the environment.
54
+
55
+ Parameters
56
+ ----------
57
+ target_package : str
58
+ The name of the package to search for dependencies on (e.g., 'panel').
59
+ ctx : Context
60
+ FastMCP context for logging and debugging.
61
+
62
+ Returns
63
+ -------
64
+ list[str]
65
+ Sorted list of package names that depend on the target package.
66
+ """
67
+ dependent_packages = []
68
+
69
+ for dist in distributions():
70
+ if dist.requires:
71
+ dist_name = dist.metadata["Name"]
72
+ await ctx.debug(f"Checking package: {dist_name} for dependencies on {target_package}")
73
+ for requirement_str in dist.requires:
74
+ if "extra ==" in requirement_str:
75
+ continue
76
+ package_name = requirement_str.split()[0].split(";")[0].split(">=")[0].split("==")[0].split("!=")[0].split("<")[0].split(">")[0].split("~")[0]
77
+ if package_name.lower() == target_package.lower():
78
+ dependent_packages.append(dist_name.replace("-", "_"))
79
+ break
80
+
81
+ return sorted(set(dependent_packages))
82
+
83
+
84
+ COMPONENTS: list[ComponentDetails] = []
85
+
86
+
87
+ async def _get_all_components(ctx: Context) -> list[ComponentDetails]:
88
+ """
89
+ Get all available Panel components from discovered packages.
90
+
91
+ This function initializes and caches the global COMPONENTS list by:
92
+ 1. Discovering all packages that depend on Panel
93
+ 2. Importing those packages to register their components
94
+ 3. Collecting detailed information about all Panel components
95
+
96
+ This is called lazily to populate the component cache when needed.
97
+
98
+ Parameters
99
+ ----------
100
+ ctx : Context
101
+ FastMCP context for logging and debugging.
102
+
103
+ Returns
104
+ -------
105
+ list[ComponentDetails]
106
+ Complete list of all discovered Panel components with detailed metadata.
107
+ """
108
+ global COMPONENTS
109
+ if not COMPONENTS:
110
+ packages_depending_on_panel = await _list_packages_depending_on("panel", ctx=ctx)
111
+
112
+ await ctx.info(f"Discovered {len(packages_depending_on_panel)} packages depending on Panel: {packages_depending_on_panel}")
113
+
114
+ for package in packages_depending_on_panel:
115
+ try:
116
+ __import__(package)
117
+ except ImportError as e:
118
+ await ctx.warning(f"Discovered but failed to import {package}: {e}")
119
+
120
+ COMPONENTS = _get_components_org()
121
+
122
+ return COMPONENTS
123
+
124
+
125
+ @mcp.tool
126
+ async def list_packages(ctx: Context) -> list[str]:
127
+ """
128
+ List all installed packages that provide Panel UI components.
129
+
130
+ Use this tool to discover what Panel-related packages are available in your environment.
131
+ This helps you understand which packages you can use in the 'package' parameter of other tools.
132
+
133
+ Parameters
134
+ ----------
135
+ ctx : Context
136
+ FastMCP context (automatically provided by the MCP framework).
137
+
138
+ Returns
139
+ -------
140
+ list[str]
141
+ List of package names that provide Panel components, sorted alphabetically.
142
+ Examples: ["panel"], ["panel", "panel_material_ui"], ["panel", "awesome_panel_extensions"]
143
+
144
+ Examples
145
+ --------
146
+ Use this tool to see available packages:
147
+ >>> list_packages()
148
+ ["panel", "panel_material_ui", "awesome_panel_extensions"]
149
+
150
+ Then use those package names in other tools:
151
+ >>> list_components(package="panel_material_ui")
152
+ >>> search("button", package="panel")
153
+ """
154
+ return sorted(set(component.project for component in await _get_all_components(ctx)))
155
+
156
+
157
+ @mcp.tool
158
+ async def search(ctx: Context, query: str, project: str | None = None, limit: int = 10) -> list[ComponentSummarySearchResult]:
159
+ """
160
+ Search for Panel components by name, module path, or description.
161
+
162
+ Use this tool to find components when you don't know the exact name but have keywords.
163
+ The search looks through component names, module paths, and documentation to find matches.
164
+
165
+ Parameters
166
+ ----------
167
+ ctx : Context
168
+ FastMCP context (automatically provided by the MCP framework).
169
+ query : str
170
+ Search term to look for. Can be component names, functionality keywords, or descriptions.
171
+ Examples: "button", "input", "text", "chart", "plot", "slider", "select"
172
+ package : str, optional
173
+ Package name to filter results. If None, searches all packages.
174
+ Examples: "panel", "panel_material_ui", "awesome_panel_extensions"
175
+ limit : int, optional
176
+ Maximum number of results to return. Default is 10.
177
+
178
+ Returns
179
+ -------
180
+ list[ComponentSummarySearchResult]
181
+ List of matching components with relevance scores (0-100, where 100 is exact match).
182
+ Results are sorted by relevance score in descending order.
183
+
184
+ Examples
185
+ --------
186
+ Search for button components:
187
+ >>> search("button")
188
+ [ComponentSummarySearchResult(name="Button", package="panel", relevance_score=80, ...)]
189
+
190
+ Search within a specific package:
191
+ >>> search("input", package="panel_material_ui")
192
+ [ComponentSummarySearchResult(name="TextInput", package="panel_material_ui", ...)]
193
+
194
+ Find chart components with limited results:
195
+ >>> search("chart", limit=5)
196
+ [ComponentSummarySearchResult(name="Bokeh", package="panel", ...)]
197
+ """
198
+ query_lower = query.lower()
199
+
200
+ matches = []
201
+ for component in await _get_all_components(ctx=ctx):
202
+ score = 0
203
+ if project and component.project.lower() != project.lower():
204
+ continue
205
+
206
+ if component.name.lower() == query_lower or component.module_path.lower() == query_lower:
207
+ score = 100
208
+ elif query_lower in component.name.lower():
209
+ score = 80
210
+ elif query_lower in component.module_path.lower():
211
+ score = 60
212
+ elif query_lower in component.docstring.lower():
213
+ score = 40
214
+ elif any(word in component.docstring.lower() for word in query_lower.split()):
215
+ score = 20
216
+
217
+ if score > 0:
218
+ matches.append(ComponentSummarySearchResult.from_component(component=component, relevance_score=score))
219
+
220
+ matches.sort(key=lambda x: x.relevance_score, reverse=True)
221
+ if len(matches) > limit:
222
+ matches = matches[:limit]
223
+
224
+ return matches
225
+
226
+
227
+ async def _get_component(ctx: Context, name: str | None = None, module_path: str | None = None, project: str | None = None) -> list[ComponentDetails]:
228
+ """
229
+ Get component details based on filtering criteria.
230
+
231
+ This is an internal function used by the public component tools to filter
232
+ and retrieve components based on name, module path, and project criteria.
233
+
234
+ Parameters
235
+ ----------
236
+ ctx : Context
237
+ FastMCP context for logging and debugging.
238
+ name : str, optional
239
+ Component name to filter by (case-insensitive). If None, all components match.
240
+ module_path : str, optional
241
+ Module path prefix to filter by. If None, all components match.
242
+ package : str, optional
243
+ Package name to filter by. If None, all components match.
244
+
245
+ Returns
246
+ -------
247
+ list[ComponentDetails]
248
+ List of components matching the specified criteria.
249
+ """
250
+ components_list = []
251
+
252
+ for component in await _get_all_components(ctx=ctx):
253
+ if name and component.name.lower() != name.lower():
254
+ continue
255
+ if project and component.project != project:
256
+ continue
257
+ if module_path and not component.module_path.startswith(module_path):
258
+ continue
259
+ components_list.append(component)
260
+
261
+ return components_list
262
+
263
+
264
+ @mcp.tool
265
+ async def list_components(ctx: Context, name: str | None = None, module_path: str | None = None, project: str | None = None) -> list[ComponentSummary]:
266
+ """
267
+ Get a summary list of Panel components without detailed docstring and parameter information.
268
+
269
+ Use this tool to get an overview of available Panel components when you want to browse
270
+ or discover components without needing full parameter details. This is faster than
271
+ get_component and provides just the essential information.
272
+
273
+ Parameters
274
+ ----------
275
+ ctx : Context
276
+ FastMCP context (automatically provided by the MCP framework).
277
+ name : str, optional
278
+ Component name to filter by (case-insensitive). If None, returns all components.
279
+ Examples: "Button", "TextInput", "Slider"
280
+ module_path : str, optional
281
+ Module path prefix to filter by. If None, returns all components.
282
+ Examples: "panel.widgets" to get all widgets, "panel.pane" to get all panes
283
+ package : str, optional
284
+ Package name to filter by. If None, returns all components.
285
+ Examples: "panel", "panel_material_ui", "awesome_panel_extensions"
286
+
287
+ Returns
288
+ -------
289
+ list[ComponentSummary]
290
+ List of component summaries containing name, package, description, and module path.
291
+ No parameter details are included for faster responses.
292
+
293
+ Examples
294
+ --------
295
+ Get all available components:
296
+ >>> list_components()
297
+ [ComponentSummary(name="Button", package="panel", description="A clickable button widget", ...)]
298
+
299
+ Get all Material UI components:
300
+ >>> list_components(package="panel_material_ui")
301
+ [ComponentSummary(name="Button", package="panel_material_ui", ...)]
302
+
303
+ Get all Button components from all packages:
304
+ >>> list_components(name="Button")
305
+ [ComponentSummary(name="Button", package="panel", ...), ComponentSummary(name="Button", package="panel_material_ui", ...)]
306
+ """
307
+ components_list = []
308
+
309
+ for component in await _get_all_components(ctx=ctx):
310
+ if name and component.name.lower() != name.lower():
311
+ continue
312
+ if project and component.project != project:
313
+ continue
314
+ if module_path and not component.module_path.startswith(module_path):
315
+ continue
316
+ components_list.append(component.to_base())
317
+
318
+ return components_list
319
+
320
+
321
+ @mcp.tool
322
+ async def get_component(ctx: Context, name: str | None = None, module_path: str | None = None, project: str | None = None) -> ComponentDetails:
323
+ """
324
+ Get complete details about a single Panel component including docstring and parameters.
325
+
326
+ Use this tool when you need full information about a specific Panel component, including
327
+ its docstring, parameter specifications, and initialization signature. This is the most
328
+ comprehensive tool for component information.
329
+
330
+ IMPORTANT: This tool returns exactly one component. If your criteria match multiple components,
331
+ you'll get an error asking you to be more specific.
332
+
333
+ Parameters
334
+ ----------
335
+ ctx : Context
336
+ FastMCP context (automatically provided by the MCP framework).
337
+ name : str, optional
338
+ Component name to match (case-insensitive). If None, must specify other criteria.
339
+ Examples: "Button", "TextInput", "Slider"
340
+ module_path : str, optional
341
+ Full module path to match. If None, uses name and package to find component.
342
+ Examples: "panel.widgets.Button", "panel_material_ui.Button"
343
+ package : str, optional
344
+ Package name to filter by. If None, searches all packages.
345
+ Examples: "panel", "panel_material_ui", "awesome_panel_extensions"
346
+
347
+ Returns
348
+ -------
349
+ ComponentDetails
350
+ Complete component information including docstring, parameters, and initialization signature.
351
+
352
+ Raises
353
+ ------
354
+ ValueError
355
+ If no components match the criteria or if multiple components match (be more specific).
356
+
357
+ Examples
358
+ --------
359
+ Get Panel's Button component:
360
+ >>> get_component(name="Button", package="panel")
361
+ ComponentDetails(name="Button", package="panel", docstring="A clickable button...", parameters={...})
362
+
363
+ Get Material UI Button component:
364
+ >>> get_component(name="Button", package="panel_material_ui")
365
+ ComponentDetails(name="Button", package="panel_material_ui", ...)
366
+
367
+ Get component by exact module path:
368
+ >>> get_component(module_path="panel.widgets.button.Button")
369
+ ComponentDetails(name="Button", module_path="panel.widgets.button.Button", ...)
370
+ """
371
+ components_list = await _get_component(ctx, name, module_path, project)
372
+
373
+ if not components_list:
374
+ raise ValueError(f"No components found matching criteria: '{name}', '{module_path}', '{project}'. Please check your inputs.")
375
+ if len(components_list) > 1:
376
+ module_paths = "'" + "','".join([component.module_path for component in components_list]) + "'"
377
+ raise ValueError(f"Multiple components found matching criteria: {module_paths}. Please refine your search.")
378
+
379
+ component = components_list[0]
380
+ return component
381
+
382
+
383
+ @mcp.tool
384
+ async def get_component_parameters(ctx: Context, name: str | None = None, module_path: str | None = None, project: str | None = None) -> dict[str, ParameterInfo]:
385
+ """
386
+ Get detailed parameter information for a single Panel component.
387
+
388
+ Use this tool when you need to understand the parameters of a specific Panel component,
389
+ including their types, default values, documentation, and constraints. This is useful
390
+ for understanding how to properly initialize and configure a component.
391
+
392
+ IMPORTANT: This tool returns parameters for exactly one component. If your criteria
393
+ match multiple components, you'll get an error asking you to be more specific.
394
+
395
+ Parameters
396
+ ----------
397
+ ctx : Context
398
+ FastMCP context (automatically provided by the MCP framework).
399
+ name : str, optional
400
+ Component name to match (case-insensitive). If None, must specify other criteria.
401
+ Examples: "Button", "TextInput", "Slider"
402
+ module_path : str, optional
403
+ Full module path to match. If None, uses name and package to find component.
404
+ Examples: "panel.widgets.Button", "panel_material_ui.Button"
405
+ package : str, optional
406
+ Package name to filter by. If None, searches all packages.
407
+ Examples: "panel", "panel_material_ui", "awesome_panel_extensions"
408
+
409
+ Returns
410
+ -------
411
+ dict[str, ParameterInfo]
412
+ Dictionary mapping parameter names to their detailed information, including:
413
+ - type: Parameter type (e.g., 'String', 'Number', 'Boolean')
414
+ - default: Default value
415
+ - doc: Parameter documentation
416
+ - bounds: Value constraints for numeric parameters
417
+ - objects: Available options for selector parameters
418
+
419
+ Raises
420
+ ------
421
+ ValueError
422
+ If no components match the criteria or if multiple components match (be more specific).
423
+
424
+ Examples
425
+ --------
426
+ Get Button parameters:
427
+ >>> get_component_parameters(name="Button", package="panel")
428
+ {"name": ParameterInfo(type="String", default="Button", doc="The text displayed on the button"), ...}
429
+
430
+ Get TextInput parameters:
431
+ >>> get_component_parameters(name="TextInput", package="panel")
432
+ {"value": ParameterInfo(type="String", default="", doc="The current text value"), ...}
433
+
434
+ Get parameters by exact module path:
435
+ >>> get_component_parameters(module_path="panel.widgets.Slider")
436
+ {"start": ParameterInfo(type="Number", default=0, bounds=(0, 100)), ...}
437
+ """
438
+ components_list = await _get_component(ctx, name, module_path, project)
439
+
440
+ if not components_list:
441
+ raise ValueError(f"No components found matching criteria: '{name}', '{module_path}', '{project}'. Please check your inputs.")
442
+ if len(components_list) > 1:
443
+ module_paths = "'" + "','".join([component.module_path for component in components_list]) + "'"
444
+ raise ValueError(f"Multiple components found matching criteria: {module_paths}. Please refine your search.")
445
+
446
+ component = components_list[0]
447
+ return component.parameters
448
+
449
+
450
+ # Maps port to server state: { 'proc': Popen, 'log_path': str, 'log_file': file, 'proxy_url': str }
451
+ _SERVERS = {}
452
+ # Lock for thread safety
453
+ _SERVER_LOCK = threading.Lock()
454
+
455
+
456
+ def _serve_impl(
457
+ file: str,
458
+ dev: bool = True,
459
+ show: bool = True,
460
+ port: int = 5007,
461
+ dependencies: Optional[list[str]] = None,
462
+ python: str | None = None,
463
+ ) -> str:
464
+ """
465
+ Start the Panel server and serve the file in a new, isolated Python environment using uv.
466
+
467
+ Parameters
468
+ ----------
469
+ file : str
470
+ Path to the Python script to serve.
471
+ dev : bool, optional
472
+ Whether to run in development mode (default: True).
473
+ show : bool, optional
474
+ Whether to open the application in a web browser (default: True).
475
+ port : int, optional
476
+ Port to serve the application on (default: 5007).
477
+ dependencies : list of str, optional
478
+ List of Python dependencies required by the script. Defaults to ["panel", "hvplot", "pandas", "watchfiles"].
479
+ python : str or None, optional
480
+ Python version to use for serving the application.
481
+
482
+ Returns
483
+ -------
484
+ str
485
+ Proxy-accessible URL where the application is served.
486
+ """
487
+ DEFAULT_DEPENDENCIES = ["panel", "hvplot", "pandas", "watchfiles"]
488
+ SERVER_START_TIMEOUT = 5 # seconds
489
+ SERVER_LOG_POLL_INTERVAL = 0.250 # seconds
490
+
491
+ if dependencies is None:
492
+ dependencies = DEFAULT_DEPENDENCIES.copy()
493
+ with _SERVER_LOCK:
494
+ if port in _SERVERS:
495
+ _close_server_impl(port)
496
+
497
+ cmd = ["uv", "run", "--no-project"]
498
+ for dep in dependencies:
499
+ cmd += ["--with", dep]
500
+ if python:
501
+ cmd += ["--python", python]
502
+ cmd += ["panel", "serve", file, "--port", str(port)]
503
+ if dev:
504
+ cmd.append("--dev")
505
+ # Do NOT use --show, we will open the browser ourselves after proxy URL resolution
506
+ logging.info(f"Panel server command: {cmd}")
507
+ log_path = f"/tmp/panel_server_{port}.log"
508
+ log_file = open(log_path, "w")
509
+ proc = subprocess.Popen(
510
+ cmd,
511
+ stdout=log_file,
512
+ stderr=subprocess.STDOUT,
513
+ )
514
+
515
+ url = f"http://localhost:{port}"
516
+ proxy_url = to_proxy_url(url, _config.server.jupyter_server_proxy_url)
517
+
518
+ _SERVERS[port] = {
519
+ "proc": proc,
520
+ "log_path": log_path,
521
+ "log_file": log_file,
522
+ "proxy_url": proxy_url,
523
+ }
524
+
525
+ if show:
526
+ import time
527
+
528
+ start_time = time.time()
529
+ last_pos = 0
530
+ while time.time() - start_time < SERVER_START_TIMEOUT:
531
+ if os.path.exists(log_path):
532
+ with open(log_path, "r") as f:
533
+ f.seek(last_pos)
534
+ new_lines = f.readlines()
535
+ if new_lines:
536
+ for line in new_lines:
537
+ logging.info(line.rstrip())
538
+ last_pos = f.tell()
539
+ with open(log_path, "r") as f:
540
+ if "Bokeh app running" in f.read():
541
+ time.sleep(0.1) # from experience
542
+ break
543
+ time.sleep(SERVER_LOG_POLL_INTERVAL)
544
+ import webbrowser
545
+
546
+ webbrowser.open_new_tab(proxy_url)
547
+ return proxy_url
548
+
549
+
550
+ @mcp.tool(enabled=bool(_config.server.security.allow_code_execution))
551
+ def serve(
552
+ file: str,
553
+ dev: bool = True,
554
+ show: bool = True,
555
+ port: int = 5007,
556
+ dependencies: Optional[list[str]] = None,
557
+ python: str | None = None,
558
+ ) -> str:
559
+ """
560
+ Start the Panel server and serve the file in a new, isolated Python environment using uv.
561
+
562
+ Parameters
563
+ ----------
564
+ file : str
565
+ Path to the Python script to serve.
566
+ dev : bool, optional
567
+ Whether to run in development mode (default: True).
568
+ show : bool, optional
569
+ Whether to open the application in a web browser (default: True).
570
+ port : int, optional
571
+ Port to serve the application on (default: 5007).
572
+ dependencies : list of str, optional
573
+ List of Python dependencies required by the script. Defaults to ["panel", "hvplot", "pandas", "watchfiles"].
574
+ python : str or None, optional
575
+ Python version to use for serving the application.
576
+
577
+ Returns
578
+ -------
579
+ str
580
+ Proxy-accessible URL where the application is served.
581
+ """
582
+ return _serve_impl(file, dev, show, port, dependencies, python)
583
+
584
+
585
+ def _get_server_logs_impl(port: int = 5007, tail: int = 100) -> str:
586
+ server = _SERVERS.get(port)
587
+ log_path = str(server["log_path"]) if server else None
588
+ if log_path and os.path.exists(log_path):
589
+ with open(log_path, "r") as f:
590
+ lines = f.readlines()
591
+ lines = [line for line in lines if "(FIXED_SIZING_MODE)" not in line and "Dropping a patch" not in line]
592
+ if tail is not None and tail > 0:
593
+ lines = lines[-tail:]
594
+ return "".join(lines)
595
+ return "No logs found."
596
+
597
+
598
+ @mcp.tool(enabled=bool(_config.server.security.allow_code_execution))
599
+ def get_server_logs(port: int = 5007, tail: int = 100) -> str:
600
+ """
601
+ Get the logs for the Panel application running on the given port.
602
+
603
+ For example after change a served 'file' or its dependencies, you can check the logs to see if the server
604
+ restarted successfully or you need to fix some errors or restart the server to add new dependencies.
605
+
606
+ Args:
607
+ port (int): Port where the application is served.
608
+ tail (int): Number of lines from the end of the log file to return. If <= 0, return all lines.
609
+
610
+ Returns
611
+ -------
612
+ str: Contents of the application logs.
613
+ """
614
+ return _get_server_logs_impl(port, tail)
615
+
616
+
617
+ def _close_server_impl(port: int = 5007) -> None:
618
+ with _SERVER_LOCK:
619
+ server = _SERVERS.pop(port, None)
620
+ if not server:
621
+ return
622
+ proc = cast(subprocess.Popen, server.get("proc"))
623
+
624
+ log_file = server.get("log_file")
625
+ if proc:
626
+ proc.terminate()
627
+ try:
628
+ proc.wait(timeout=10)
629
+ except Exception:
630
+ proc.kill()
631
+ if log_file:
632
+ try:
633
+ log_file.close() # type: ignore[attr-defined]
634
+ except Exception:
635
+ pass
636
+
637
+
638
+ @mcp.tool(enabled=bool(_config.server.security.allow_code_execution))
639
+ def close_server(port: int = 5007) -> None:
640
+ """
641
+ Close the Panel application server running on the given port and clean up the log file handle.
642
+
643
+ Args:
644
+ port (int): Port where the application is served.
645
+ """
646
+ return _close_server_impl(port)
647
+
648
+
649
+ if __name__ == "__main__":
650
+ mcp.run(transport=_config.server.transport)
holoviz_mcp/py.typed ADDED
File without changes
holoviz_mcp/serve.py ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """Serve Panel apps with configurable arguments."""
3
+
4
+ import logging
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def main() -> None:
15
+ """Serve all Panel apps in the apps directory."""
16
+ apps_dir = Path(__file__).parent / "apps"
17
+ app_files = [str(f) for f in apps_dir.glob("*.py") if f.name != "__init__.py"]
18
+
19
+ if not app_files:
20
+ logger.warning("No Panel apps found in apps directory")
21
+ return
22
+
23
+ # Use python -m panel to ensure we use the same Python environment
24
+ cmd = [sys.executable, "-m", "panel", "serve", *app_files, *sys.argv[1:]]
25
+ logger.info(f"Running: {' '.join(cmd)}")
26
+
27
+ try:
28
+ subprocess.run(cmd, check=True)
29
+ except KeyboardInterrupt:
30
+ logger.info("Server stopped")
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()