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.
- holoviz_mcp/__init__.py +18 -0
- holoviz_mcp/apps/__init__.py +1 -0
- holoviz_mcp/apps/configuration_viewer.py +116 -0
- holoviz_mcp/apps/search.py +314 -0
- holoviz_mcp/config/__init__.py +31 -0
- holoviz_mcp/config/config.yaml +167 -0
- holoviz_mcp/config/loader.py +308 -0
- holoviz_mcp/config/models.py +216 -0
- holoviz_mcp/config/resources/best-practices/hvplot.md +62 -0
- holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
- holoviz_mcp/config/resources/best-practices/panel.md +294 -0
- holoviz_mcp/config/schema.json +203 -0
- holoviz_mcp/docs_mcp/__init__.py +1 -0
- holoviz_mcp/docs_mcp/data.py +963 -0
- holoviz_mcp/docs_mcp/models.py +21 -0
- holoviz_mcp/docs_mcp/pages_design.md +407 -0
- holoviz_mcp/docs_mcp/server.py +220 -0
- holoviz_mcp/hvplot_mcp/__init__.py +1 -0
- holoviz_mcp/hvplot_mcp/server.py +152 -0
- holoviz_mcp/panel_mcp/__init__.py +17 -0
- holoviz_mcp/panel_mcp/data.py +316 -0
- holoviz_mcp/panel_mcp/models.py +124 -0
- holoviz_mcp/panel_mcp/server.py +650 -0
- holoviz_mcp/py.typed +0 -0
- holoviz_mcp/serve.py +34 -0
- holoviz_mcp/server.py +77 -0
- holoviz_mcp/shared/__init__.py +1 -0
- holoviz_mcp/shared/extract_tools.py +74 -0
- holoviz_mcp-0.0.1a2.dist-info/METADATA +641 -0
- holoviz_mcp-0.0.1a2.dist-info/RECORD +33 -0
- {holoviz_mcp-0.0.1a0.dist-info → holoviz_mcp-0.0.1a2.dist-info}/WHEEL +1 -2
- holoviz_mcp-0.0.1a2.dist-info/entry_points.txt +4 -0
- holoviz_mcp-0.0.1a2.dist-info/licenses/LICENSE.txt +30 -0
- holoviz_mcp-0.0.1a0.dist-info/METADATA +0 -6
- holoviz_mcp-0.0.1a0.dist-info/RECORD +0 -5
- holoviz_mcp-0.0.1a0.dist-info/top_level.txt +0 -1
- 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()
|