holoviz-mcp 0.4.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.
Files changed (56) 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/holoviz_get_best_practices.py +173 -0
  5. holoviz_mcp/apps/holoviz_search.py +319 -0
  6. holoviz_mcp/apps/hvplot_get_docstring.py +255 -0
  7. holoviz_mcp/apps/hvplot_get_signature.py +252 -0
  8. holoviz_mcp/apps/hvplot_list_plot_types.py +83 -0
  9. holoviz_mcp/apps/panel_get_component.py +496 -0
  10. holoviz_mcp/apps/panel_get_component_parameters.py +467 -0
  11. holoviz_mcp/apps/panel_list_components.py +311 -0
  12. holoviz_mcp/apps/panel_list_packages.py +71 -0
  13. holoviz_mcp/apps/panel_search_components.py +312 -0
  14. holoviz_mcp/cli.py +75 -0
  15. holoviz_mcp/client.py +94 -0
  16. holoviz_mcp/config/__init__.py +29 -0
  17. holoviz_mcp/config/config.yaml +178 -0
  18. holoviz_mcp/config/loader.py +316 -0
  19. holoviz_mcp/config/models.py +208 -0
  20. holoviz_mcp/config/resources/best-practices/holoviews.md +423 -0
  21. holoviz_mcp/config/resources/best-practices/hvplot.md +465 -0
  22. holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
  23. holoviz_mcp/config/resources/best-practices/panel.md +562 -0
  24. holoviz_mcp/config/schema.json +228 -0
  25. holoviz_mcp/holoviz_mcp/__init__.py +1 -0
  26. holoviz_mcp/holoviz_mcp/data.py +970 -0
  27. holoviz_mcp/holoviz_mcp/models.py +21 -0
  28. holoviz_mcp/holoviz_mcp/pages_design.md +407 -0
  29. holoviz_mcp/holoviz_mcp/server.py +220 -0
  30. holoviz_mcp/hvplot_mcp/__init__.py +1 -0
  31. holoviz_mcp/hvplot_mcp/server.py +146 -0
  32. holoviz_mcp/panel_mcp/__init__.py +17 -0
  33. holoviz_mcp/panel_mcp/data.py +319 -0
  34. holoviz_mcp/panel_mcp/models.py +124 -0
  35. holoviz_mcp/panel_mcp/server.py +443 -0
  36. holoviz_mcp/py.typed +0 -0
  37. holoviz_mcp/serve.py +36 -0
  38. holoviz_mcp/server.py +86 -0
  39. holoviz_mcp/shared/__init__.py +1 -0
  40. holoviz_mcp/shared/extract_tools.py +74 -0
  41. holoviz_mcp/thumbnails/configuration_viewer.png +0 -0
  42. holoviz_mcp/thumbnails/holoviz_get_best_practices.png +0 -0
  43. holoviz_mcp/thumbnails/holoviz_search.png +0 -0
  44. holoviz_mcp/thumbnails/hvplot_get_docstring.png +0 -0
  45. holoviz_mcp/thumbnails/hvplot_get_signature.png +0 -0
  46. holoviz_mcp/thumbnails/hvplot_list_plot_types.png +0 -0
  47. holoviz_mcp/thumbnails/panel_get_component.png +0 -0
  48. holoviz_mcp/thumbnails/panel_get_component_parameters.png +0 -0
  49. holoviz_mcp/thumbnails/panel_list_components.png +0 -0
  50. holoviz_mcp/thumbnails/panel_list_packages.png +0 -0
  51. holoviz_mcp/thumbnails/panel_search_components.png +0 -0
  52. holoviz_mcp-0.4.0.dist-info/METADATA +216 -0
  53. holoviz_mcp-0.4.0.dist-info/RECORD +56 -0
  54. holoviz_mcp-0.4.0.dist-info/WHEEL +4 -0
  55. holoviz_mcp-0.4.0.dist-info/entry_points.txt +2 -0
  56. holoviz_mcp-0.4.0.dist-info/licenses/LICENSE.txt +30 -0
@@ -0,0 +1,311 @@
1
+ """An app to list and filter Panel components.
2
+
3
+ An interactive version of the holoviz_mcp.panel_mcp.server.list_components tool.
4
+ """
5
+
6
+ import pandas as pd
7
+ import panel as pn
8
+ import panel_material_ui as pmui
9
+ import param
10
+
11
+ from holoviz_mcp.client import call_tool
12
+
13
+ pn.extension()
14
+
15
+ pn.pane.Markdown.disable_anchors = True
16
+
17
+ ALL = "All Packages"
18
+
19
+ ABOUT = """
20
+ ## Panel Component List Tool
21
+
22
+ This tool provides an interactive interface for listing and filtering Panel components.
23
+
24
+ ### How to Use This Tool
25
+
26
+ 1. **Filter by Name**: Enter a component name (e.g., "Button") for exact case-insensitive matching
27
+ 2. **Filter by Module Path**: Enter a module path prefix (e.g., "panel.widgets") to list all components in that module
28
+ 3. **Filter by Package**: Select a specific package or "All Packages" to see components from all packages
29
+ 4. **Click List Components**: View all matching components
30
+
31
+ Leave filters empty to see all components.
32
+
33
+ ### Key Differences from Search Tool
34
+
35
+ Unlike the search tool which uses fuzzy matching and relevance scoring, this tool:
36
+ - Uses **exact/prefix matching** for precise filtering
37
+ - Returns **ALL matching components** (no result limit)
38
+ - Provides **faster responses** (no docstring content included)
39
+ - Best for **browsing** when you know what category you're looking for
40
+
41
+ ### Results Display
42
+
43
+ Each result includes:
44
+ - **Component Name**: The class name of the component
45
+ - **Package**: Which Panel package contains it (e.g., "panel", "panel_material_ui")
46
+ - **Module Path**: The full Python import path
47
+ - **Description**: A brief summary of the component's functionality
48
+
49
+ You can click column headers to sort the results alphabetically.
50
+
51
+ ### Learn More
52
+
53
+ For more information about this project, including setup instructions and advanced configuration options,
54
+ visit: [HoloViz MCP](https://marcskovmadsen.github.io/holoviz-mcp/).
55
+ """
56
+
57
+
58
+ class ListComponentsConfiguration(param.Parameterized):
59
+ """Configuration for Panel component list tool."""
60
+
61
+ component_name = param.String(default="Button", label="Component Name")
62
+ module_path = param.String(default="", label="Module Path")
63
+ package = param.Selector(default=ALL, objects=[ALL], label="Package")
64
+
65
+ list_components = param.Event(label="List Components")
66
+
67
+ results = param.List(default=[], doc="List results")
68
+ loading = param.Boolean(default=False, doc="Loading state")
69
+ error_message = param.String(default="", doc="Error message if listing fails")
70
+
71
+ def __init__(self, **params):
72
+ super().__init__(**params)
73
+ # Initialize with available packages
74
+ pn.state.execute(self._update_packages)
75
+
76
+ if pn.state.location:
77
+ pn.state.location.sync(self, parameters=["component_name", "module_path", "package"])
78
+
79
+ async def _update_packages(self):
80
+ """Update the available Panel packages."""
81
+ result = await call_tool("panel_list_packages", {})
82
+ packages = [p for p in result.data]
83
+ self.param.package.objects = [ALL] + sorted(packages)
84
+
85
+ @param.depends("list_components", watch=True)
86
+ async def _update_results(self):
87
+ """Execute list_components and update results."""
88
+ self.loading = True
89
+ self.error_message = ""
90
+ self.results = []
91
+
92
+ params = {}
93
+
94
+ # Add filters only if they have values
95
+ if self.component_name.strip():
96
+ params["name"] = self.component_name.strip()
97
+ if self.module_path.strip():
98
+ params["module_path"] = self.module_path.strip()
99
+ if self.package != ALL:
100
+ params["package"] = self.package
101
+
102
+ try:
103
+ result = await call_tool("panel_list_components", params)
104
+
105
+ if result and hasattr(result, "data"):
106
+ self.results = result.data if result.data else []
107
+ if not self.results:
108
+ self.error_message = "No components found matching the filters"
109
+ else:
110
+ self.error_message = "List returned no data"
111
+
112
+ except Exception as e:
113
+ self.error_message = f"List failed: {str(e)}"
114
+ finally:
115
+ self.loading = False
116
+
117
+
118
+ class ComponentsListViewer(pn.viewable.Viewer):
119
+ """Viewer for displaying component list as a table."""
120
+
121
+ results = param.List(default=[], allow_refs=True, doc="List of components")
122
+
123
+ data = param.DataFrame(doc="DataFrame of components")
124
+
125
+ def __init__(self, **params):
126
+ super().__init__(**params)
127
+
128
+ no_components_message = pn.pane.Markdown(
129
+ "### No results to display.\n" "Click 'List Components' to see all available components, or use filters to narrow your search.",
130
+ sizing_mode="stretch_width",
131
+ visible=self.is_empty,
132
+ )
133
+
134
+ titles = {
135
+ "name": "Component",
136
+ "package": "Package",
137
+ "module_path": "Path",
138
+ "description": "Description",
139
+ }
140
+ formatters = {"description": "textarea"}
141
+ table = pn.widgets.Tabulator(
142
+ self.param.data,
143
+ titles=titles,
144
+ formatters=formatters,
145
+ sizing_mode="stretch_width",
146
+ show_index=False,
147
+ name="Human Readable View",
148
+ disabled=True,
149
+ sortable=True,
150
+ )
151
+
152
+ response = pn.pane.JSON(
153
+ self.param.results,
154
+ depth=3,
155
+ sizing_mode="stretch_width",
156
+ theme="dark",
157
+ name="Raw Response",
158
+ )
159
+
160
+ tabs = pn.Tabs(table, response, visible=self.is_not_empty, sizing_mode="stretch_width")
161
+
162
+ self._layout = pmui.Column(
163
+ no_components_message,
164
+ tabs,
165
+ sizing_mode="stretch_width",
166
+ )
167
+
168
+ @param.depends("results")
169
+ def is_empty(self):
170
+ """Check if there are no results."""
171
+ return not bool(self.results)
172
+
173
+ @param.depends("results")
174
+ def is_not_empty(self):
175
+ """Check if there are results."""
176
+ return bool(self.results)
177
+
178
+ @param.depends("results", watch=True)
179
+ def _update_data(self):
180
+ if not self.results:
181
+ data = pd.DataFrame(columns=["name", "package", "module_path", "description"])
182
+ else:
183
+ data = pd.DataFrame(self.results)[["name", "package", "module_path", "description"]]
184
+ self.data = data
185
+
186
+ def __panel__(self):
187
+ """Return the Panel layout."""
188
+ return self._layout
189
+
190
+
191
+ class PanelListComponentsApp(pn.viewable.Viewer):
192
+ """Main application for listing and filtering Panel components."""
193
+
194
+ title = param.String(default="HoloViz MCP - panel_list_components Tool Demo")
195
+
196
+ def __init__(self, **params):
197
+ super().__init__(**params)
198
+
199
+ # Create configuration and components
200
+ self._config = ListComponentsConfiguration()
201
+ self._components_list = ComponentsListViewer(results=self._config.param.results)
202
+
203
+ with pn.config.set(sizing_mode="stretch_width"):
204
+ self._name_input = pmui.TextInput.from_param(
205
+ self._config.param.component_name,
206
+ label="Component Name",
207
+ placeholder="e.g., Button, TextInput, Slider...",
208
+ variant="outlined",
209
+ sx={"width": "100%"},
210
+ )
211
+
212
+ self._module_path_input = pmui.TextInput.from_param(
213
+ self._config.param.module_path,
214
+ label="Module Path",
215
+ placeholder="e.g., panel.widgets, panel.pane...",
216
+ variant="outlined",
217
+ sx={"width": "100%"},
218
+ )
219
+
220
+ self._package_select = pmui.Select.from_param(
221
+ self._config.param.package,
222
+ label="Package",
223
+ variant="outlined",
224
+ sx={"width": "100%"},
225
+ )
226
+
227
+ self._list_button = pmui.Button.from_param(
228
+ self._config.param.list_components,
229
+ label="List Components",
230
+ color="primary",
231
+ variant="contained",
232
+ sx={"width": "100%", "marginTop": "10px"},
233
+ on_click=lambda e: self._config.param.trigger("list_components"),
234
+ )
235
+
236
+ # Status indicators
237
+ self._status_pane = pn.pane.Markdown(self._status_text, sizing_mode="stretch_width")
238
+ self._error_pane = pmui.Alert(
239
+ self._error_text,
240
+ alert_type="error",
241
+ visible=pn.rx(bool)(self._config.param.error_message),
242
+ sizing_mode="stretch_width",
243
+ )
244
+
245
+ # Create static layout structure
246
+ self._sidebar = pn.Column(
247
+ pmui.Typography("## Filter Options", variant="h6"),
248
+ self._name_input,
249
+ self._module_path_input,
250
+ self._package_select,
251
+ self._list_button,
252
+ self._error_pane,
253
+ self._status_pane,
254
+ sizing_mode="stretch_width",
255
+ )
256
+
257
+ self._main = pmui.Container(self._components_list, width_option="xl")
258
+
259
+ @param.depends("_config.loading", "_config.results")
260
+ def _status_text(self):
261
+ """Generate status message."""
262
+ if self._config.loading:
263
+ return "_Loading components..._"
264
+ elif self._config.results:
265
+ return f"_Found {len(self._config.results)} component(s)_"
266
+ return ""
267
+
268
+ @param.depends("_config.error_message")
269
+ def _error_text(self):
270
+ """Get error message."""
271
+ return self._config.error_message
272
+
273
+ def __panel__(self):
274
+ """Return the main page layout."""
275
+ with pn.config.set(sizing_mode="stretch_width"):
276
+ # About button and dialog
277
+ about_button = pmui.IconButton(
278
+ label="About",
279
+ icon="info",
280
+ description="Click to learn about the Panel List Components Tool.",
281
+ sizing_mode="fixed",
282
+ color="light",
283
+ margin=(10, 0),
284
+ )
285
+ about = pmui.Dialog(ABOUT, close_on_click=True, width=0)
286
+ about_button.js_on_click(args={"about": about}, code="about.data.open = true")
287
+
288
+ # GitHub button
289
+ github_button = pmui.IconButton(
290
+ label="Github",
291
+ icon="star",
292
+ description="Give HoloViz-MCP a star on GitHub",
293
+ sizing_mode="fixed",
294
+ color="light",
295
+ margin=(10, 0),
296
+ href="https://github.com/MarcSkovMadsen/holoviz-mcp",
297
+ target="_blank",
298
+ )
299
+
300
+ return pmui.Page(
301
+ title=self.title,
302
+ site_url="./",
303
+ sidebar=[self._sidebar],
304
+ header=[pn.Row(pn.Spacer(), about_button, github_button, align="end")],
305
+ main=[about, self._main],
306
+ )
307
+
308
+
309
+ if pn.state.served:
310
+ pn.extension("tabulator")
311
+ PanelListComponentsApp().servable()
@@ -0,0 +1,71 @@
1
+ """An app to demo the usage and responses of the panel_list_packages tool."""
2
+
3
+ import panel as pn
4
+ import panel_material_ui as pmui
5
+
6
+ from holoviz_mcp.client import call_tool
7
+
8
+ ABOUT = """
9
+ # Panel List Packages Tool
10
+
11
+ The `panel_list_packages` tool lists all installed packages that provide Panel `Viewable` components.
12
+
13
+ ## Purpose
14
+
15
+ Discover what Panel-related packages are available in your environment.
16
+ This helps you understand which packages you can use in the 'package' parameter of other tools.
17
+
18
+ ## Returns
19
+
20
+ A list of package names that provide Panel components, sorted alphabetically.
21
+
22
+ **Examples:** `["panel"]` or `["panel", "panel_material_ui"]`
23
+ """
24
+
25
+
26
+ @pn.cache
27
+ async def panel_list_packages() -> list[str]:
28
+ """Demo the usage and responses of the panel_list_packages tool."""
29
+ response = await call_tool(
30
+ tool_name="panel_list_packages",
31
+ parameters={},
32
+ )
33
+ return response.data
34
+
35
+
36
+ def create_app():
37
+ """Create the Panel Material UI app for demoing the panel_list_packages tool."""
38
+ about_button = pmui.IconButton(
39
+ label="About",
40
+ icon="info",
41
+ description="Click to learn about the Panel List Packages Tool.",
42
+ sizing_mode="fixed",
43
+ color="light",
44
+ margin=(10, 0),
45
+ )
46
+ about = pmui.Dialog(ABOUT, close_on_click=True, width=0)
47
+ about_button.js_on_click(args={"about": about}, code="about.data.open = true")
48
+
49
+ # GitHub button
50
+ github_button = pmui.IconButton(
51
+ label="Github",
52
+ icon="star",
53
+ description="Give HoloViz-MCP a star on GitHub",
54
+ sizing_mode="fixed",
55
+ color="light",
56
+ margin=(10, 0),
57
+ href="https://github.com/MarcSkovMadsen/holoviz-mcp",
58
+ target="_blank",
59
+ )
60
+
61
+ main = pmui.Container(about, pn.pane.JSON(panel_list_packages, theme="dark", depth=3, sizing_mode="stretch_width"))
62
+
63
+ return pmui.Page(
64
+ title="HoloViz-MCP: panel_list_packages Tool Demo",
65
+ header=[pmui.Row(pn.HSpacer(), about_button, github_button, sizing_mode="stretch_width")],
66
+ main=[main],
67
+ )
68
+
69
+
70
+ if pn.state.served:
71
+ create_app().servable()
@@ -0,0 +1,312 @@
1
+ """An app to search for Panel components and examples.
2
+
3
+ An interactive version of the holoviz_mcp.panel_mcp.server.search tool.
4
+ """
5
+
6
+ import pandas as pd
7
+ import panel as pn
8
+ import panel_material_ui as pmui
9
+ import param
10
+
11
+ from holoviz_mcp.client import call_tool
12
+
13
+ pn.extension()
14
+
15
+ pn.pane.Markdown.disable_anchors = True
16
+
17
+ ALL = "All Packages"
18
+
19
+ ABOUT = """
20
+ ## Panel Component Search Tool
21
+
22
+ The `panel_search_components` tool provides an interactive interface for searching Panel components and discovering their capabilities.
23
+
24
+ ### How to Use This Search Tool
25
+
26
+ 1. **Enter a Query**: Type component names, features, or descriptions (e.g., "Button", "input", "layout")
27
+ 2. **Filter by Package**: Narrow your search to specific Panel packages (or search all)
28
+ 3. **Set Max Results**: Control how many results to display
29
+ 4. **Click Search**: View matching components with relevance scores
30
+
31
+ ### How the Search Works
32
+
33
+ The search looks through component names, module paths, and docstrings to find matches to the *Search Query*.
34
+
35
+ ### Search Results
36
+
37
+ Each result includes:
38
+
39
+ - **Component Name**: The class name of the component
40
+ - **Relevance Score**: How closely it matches your query
41
+ - **Package**: Which Panel package contains it
42
+ - **Module Path**: The full Python import path
43
+ - **Description**: A brief summary of the component's functionality
44
+
45
+ ### Learn More
46
+
47
+ For more information about this project, including setup instructions and advanced configuration options,
48
+ visit: [HoloViz MCP](https://marcskovmadsen.github.io/holoviz-mcp/).
49
+ """
50
+
51
+
52
+ class SearchConfiguration(param.Parameterized):
53
+ """Configuration for Panel component search tool."""
54
+
55
+ query = param.String(default="Button", label="Search Query")
56
+ package = param.Selector(default=ALL, objects=[ALL], label="Package")
57
+ limit = param.Integer(default=10, bounds=(1, 50), label="Max Results")
58
+
59
+ search = param.Event(label="Search")
60
+
61
+ results = param.List(default=[], doc="Search results")
62
+ loading = param.Boolean(default=False, doc="Loading state")
63
+ error_message = param.String(default="", doc="Error message if search fails")
64
+
65
+ def __init__(self, **params):
66
+ super().__init__(**params)
67
+ if pn.state.location:
68
+ pn.state.location.sync(self, ["query", "package", "limit"])
69
+
70
+ # Initialize with available packages
71
+ pn.state.execute(self._update_packages)
72
+
73
+ async def _update_packages(self):
74
+ """Update the available Panel packages."""
75
+ result = await call_tool("panel_list_packages", {})
76
+ packages = [p for p in result.data]
77
+ self.param.package.objects = [ALL] + sorted(packages)
78
+
79
+ @param.depends("search", watch=True)
80
+ async def _update_results(self):
81
+ """Execute search and update results."""
82
+ if not self.query.strip():
83
+ self.error_message = "Please enter a search query"
84
+ return
85
+
86
+ self.loading = True
87
+ self.error_message = ""
88
+ self.results = []
89
+
90
+ params = {
91
+ "query": self.query,
92
+ "limit": self.limit,
93
+ }
94
+ # Add package filter if not "All"
95
+ if self.package != ALL:
96
+ params["package"] = self.package
97
+
98
+ try:
99
+ result = await call_tool("panel_search_components", params)
100
+
101
+ if result and hasattr(result, "data"):
102
+ self.results = result.data if result.data else []
103
+ if not self.results:
104
+ self.error_message = "No results found"
105
+ else:
106
+ self.error_message = "Search returned no data"
107
+
108
+ except Exception as e:
109
+ self.error_message = f"Search failed: {str(e)}"
110
+ finally:
111
+ self.loading = False
112
+
113
+
114
+ class SearchResultsViewer(pn.viewable.Viewer):
115
+ """Viewer for displaying search results as a menu list."""
116
+
117
+ results = param.List(default=[], allow_refs=True, doc="List of search results")
118
+
119
+ data = param.DataFrame(doc="DataFrame of search results")
120
+
121
+ def __init__(self, **params):
122
+ super().__init__(**params)
123
+
124
+ no_components_message = pn.pane.Markdown(
125
+ "### No results to display.\n" "Please enter a search query and click 'Search' to find Panel components.",
126
+ sizing_mode="stretch_width",
127
+ visible=self.is_empty,
128
+ )
129
+ titles = {
130
+ "name": "Component",
131
+ "relevance_score": "Relevance",
132
+ "package": "Package",
133
+ "module_path": "Path",
134
+ "description": "Description",
135
+ }
136
+ formatters = {"description": "textarea"}
137
+ table = pn.widgets.Tabulator(
138
+ self.param.data,
139
+ titles=titles,
140
+ formatters=formatters,
141
+ sizing_mode="stretch_width",
142
+ show_index=False,
143
+ name="Human Readable View",
144
+ disabled=True,
145
+ )
146
+
147
+ raw = pn.pane.JSON(
148
+ self.param.results,
149
+ depth=3,
150
+ theme="dark",
151
+ sizing_mode="stretch_width",
152
+ name="Raw Response",
153
+ )
154
+
155
+ tabs = pn.Tabs(table, raw, visible=self.is_not_empty, sizing_mode="stretch_width")
156
+
157
+ self._layout = pmui.Column(
158
+ no_components_message,
159
+ tabs,
160
+ sizing_mode="stretch_width",
161
+ )
162
+
163
+ @param.depends("results")
164
+ def is_empty(self):
165
+ """Check if there are no results."""
166
+ return not bool(self.results)
167
+
168
+ @param.depends("results")
169
+ def is_not_empty(self):
170
+ """Check if there are results."""
171
+ return bool(self.results)
172
+
173
+ @param.depends("results", watch=True)
174
+ def _update_data(self):
175
+ if not self.results:
176
+ data = pd.DataFrame(columns=["name", "relevance_score", "package", "module_path", "description"])
177
+ else:
178
+ data = pd.DataFrame(self.results)[["name", "relevance_score", "package", "module_path", "description"]]
179
+ self.data = data
180
+
181
+ def __panel__(self):
182
+ """Return the Panel layout."""
183
+ return self._layout
184
+
185
+
186
+ class PanelSearchApp(pn.viewable.Viewer):
187
+ """Main application for exploring Panel component search."""
188
+
189
+ title = param.String(default="HoloViz MCP - panel_search_components Tool Demo")
190
+
191
+ def __init__(self, **params):
192
+ super().__init__(**params)
193
+
194
+ # Create configuration and components
195
+ self._config = SearchConfiguration()
196
+ self._search_results = SearchResultsViewer(results=self._config.param.results)
197
+
198
+ with pn.config.set(sizing_mode="stretch_width"):
199
+ self._search_input = pmui.TextInput.from_param(
200
+ self._config.param.query,
201
+ label="Search Query",
202
+ placeholder="e.g., Button, TextInput, layout...",
203
+ variant="outlined",
204
+ sx={"width": "100%"},
205
+ )
206
+
207
+ self._package_select = pmui.Select.from_param(
208
+ self._config.param.package,
209
+ label="Package",
210
+ variant="outlined",
211
+ sx={"width": "100%"},
212
+ )
213
+
214
+ self._limit_input = pmui.IntInput.from_param(
215
+ self._config.param.limit,
216
+ label="Max Results",
217
+ variant="outlined",
218
+ sx={"width": "100%"},
219
+ )
220
+
221
+ self._search_button = pmui.Button.from_param(
222
+ self._config.param.search,
223
+ label="Search",
224
+ color="primary",
225
+ variant="contained",
226
+ sx={"width": "100%", "marginTop": "10px"},
227
+ on_click=lambda e: self._config.param.trigger("search"),
228
+ )
229
+
230
+ # Status indicators
231
+ self._status_pane = pn.pane.Markdown(self._status_text, sizing_mode="stretch_width")
232
+ self._error_pane = pmui.Alert(
233
+ self._error_text,
234
+ alert_type="error",
235
+ visible=pn.rx(bool)(self._config.param.error_message),
236
+ sizing_mode="stretch_width",
237
+ )
238
+
239
+ # Create static layout structure
240
+ self._sidebar = pn.Column(
241
+ pmui.Typography("## Search Filters", variant="h6"),
242
+ self._search_input,
243
+ self._package_select,
244
+ self._limit_input,
245
+ self._search_button,
246
+ self._error_pane,
247
+ self._status_pane,
248
+ sizing_mode="stretch_width",
249
+ )
250
+
251
+ self._main = pmui.Container(self._search_results, width_option="xl")
252
+
253
+ @param.depends("_config.loading", "_config.results")
254
+ def _status_text(self):
255
+ """Generate status message."""
256
+ if self._config.loading:
257
+ return "_Searching..._"
258
+ elif self._config.results:
259
+ return f"_Found {len(self._config.results)} result(s)_"
260
+ return ""
261
+
262
+ @param.depends("_config.error_message")
263
+ def _error_text(self):
264
+ """Get error message."""
265
+ return self._config.error_message
266
+
267
+ def __panel__(self):
268
+ """Return the main page layout."""
269
+ with pn.config.set(sizing_mode="stretch_width"):
270
+ # About button and dialog
271
+ about_button = pmui.IconButton(
272
+ label="About",
273
+ icon="info",
274
+ description="Click to learn about the Panel Search Tool.",
275
+ sizing_mode="fixed",
276
+ color="light",
277
+ margin=(10, 0),
278
+ )
279
+ about = pmui.Dialog(ABOUT, close_on_click=True, width=0)
280
+ about_button.js_on_click(args={"about": about}, code="about.data.open = true")
281
+
282
+ # GitHub button
283
+ github_button = pmui.IconButton(
284
+ label="GitHub",
285
+ icon="star",
286
+ description="Give HoloViz-MCP a star on GitHub",
287
+ sizing_mode="fixed",
288
+ color="light",
289
+ margin=(10, 0),
290
+ href="https://github.com/MarcSkovMadsen/holoviz-mcp",
291
+ target="_blank",
292
+ )
293
+
294
+ return pmui.Page(
295
+ title=self.title,
296
+ site_url="./",
297
+ sidebar=[self._sidebar],
298
+ header=[pn.Row(pn.Spacer(), about_button, github_button, align="end")],
299
+ main=[about, self._main],
300
+ )
301
+
302
+
303
+ def create_app(**params):
304
+ """Create and return the servable Panel Search app."""
305
+ app = PanelSearchApp(**params)
306
+ return app.__panel__()
307
+
308
+
309
+ # Make the app servable
310
+ if pn.state.served:
311
+ pn.extension("tabulator")
312
+ create_app().servable()