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.
- holoviz_mcp/__init__.py +18 -0
- holoviz_mcp/apps/__init__.py +1 -0
- holoviz_mcp/apps/configuration_viewer.py +116 -0
- holoviz_mcp/apps/holoviz_get_best_practices.py +173 -0
- holoviz_mcp/apps/holoviz_search.py +319 -0
- holoviz_mcp/apps/hvplot_get_docstring.py +255 -0
- holoviz_mcp/apps/hvplot_get_signature.py +252 -0
- holoviz_mcp/apps/hvplot_list_plot_types.py +83 -0
- holoviz_mcp/apps/panel_get_component.py +496 -0
- holoviz_mcp/apps/panel_get_component_parameters.py +467 -0
- holoviz_mcp/apps/panel_list_components.py +311 -0
- holoviz_mcp/apps/panel_list_packages.py +71 -0
- holoviz_mcp/apps/panel_search_components.py +312 -0
- holoviz_mcp/cli.py +75 -0
- holoviz_mcp/client.py +94 -0
- holoviz_mcp/config/__init__.py +29 -0
- holoviz_mcp/config/config.yaml +178 -0
- holoviz_mcp/config/loader.py +316 -0
- holoviz_mcp/config/models.py +208 -0
- holoviz_mcp/config/resources/best-practices/holoviews.md +423 -0
- holoviz_mcp/config/resources/best-practices/hvplot.md +465 -0
- holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
- holoviz_mcp/config/resources/best-practices/panel.md +562 -0
- holoviz_mcp/config/schema.json +228 -0
- holoviz_mcp/holoviz_mcp/__init__.py +1 -0
- holoviz_mcp/holoviz_mcp/data.py +970 -0
- holoviz_mcp/holoviz_mcp/models.py +21 -0
- holoviz_mcp/holoviz_mcp/pages_design.md +407 -0
- holoviz_mcp/holoviz_mcp/server.py +220 -0
- holoviz_mcp/hvplot_mcp/__init__.py +1 -0
- holoviz_mcp/hvplot_mcp/server.py +146 -0
- holoviz_mcp/panel_mcp/__init__.py +17 -0
- holoviz_mcp/panel_mcp/data.py +319 -0
- holoviz_mcp/panel_mcp/models.py +124 -0
- holoviz_mcp/panel_mcp/server.py +443 -0
- holoviz_mcp/py.typed +0 -0
- holoviz_mcp/serve.py +36 -0
- holoviz_mcp/server.py +86 -0
- holoviz_mcp/shared/__init__.py +1 -0
- holoviz_mcp/shared/extract_tools.py +74 -0
- holoviz_mcp/thumbnails/configuration_viewer.png +0 -0
- holoviz_mcp/thumbnails/holoviz_get_best_practices.png +0 -0
- holoviz_mcp/thumbnails/holoviz_search.png +0 -0
- holoviz_mcp/thumbnails/hvplot_get_docstring.png +0 -0
- holoviz_mcp/thumbnails/hvplot_get_signature.png +0 -0
- holoviz_mcp/thumbnails/hvplot_list_plot_types.png +0 -0
- holoviz_mcp/thumbnails/panel_get_component.png +0 -0
- holoviz_mcp/thumbnails/panel_get_component_parameters.png +0 -0
- holoviz_mcp/thumbnails/panel_list_components.png +0 -0
- holoviz_mcp/thumbnails/panel_list_packages.png +0 -0
- holoviz_mcp/thumbnails/panel_search_components.png +0 -0
- holoviz_mcp-0.4.0.dist-info/METADATA +216 -0
- holoviz_mcp-0.4.0.dist-info/RECORD +56 -0
- holoviz_mcp-0.4.0.dist-info/WHEEL +4 -0
- holoviz_mcp-0.4.0.dist-info/entry_points.txt +2 -0
- holoviz_mcp-0.4.0.dist-info/licenses/LICENSE.txt +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Data models for the HoloViz Documentation MCP server."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic import HttpUrl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Document(BaseModel):
|
|
11
|
+
"""Represents a document."""
|
|
12
|
+
|
|
13
|
+
title: str = Field(..., description="The title of the document.")
|
|
14
|
+
url: HttpUrl = Field(..., description="The URL of the rendered, target document.")
|
|
15
|
+
project: str = Field(..., description="The project to which the document belongs.")
|
|
16
|
+
source_path: str = Field(..., description="The path to the document within the project.")
|
|
17
|
+
source_url: HttpUrl = Field(..., description="The URL to the source document.")
|
|
18
|
+
is_reference: bool = Field(..., description="Indicates if the document is a reference guide.")
|
|
19
|
+
description: Optional[str] = Field(default=None, description="A brief description of the document.")
|
|
20
|
+
content: Optional[str] = Field(default=None, description="The content of the documentation, if available. In Markdown format if possible.")
|
|
21
|
+
relevance_score: Optional[float] = Field(default=None, description="Relevance score of the document, where 1 is the highest score indicating an exact match.")
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Pages Tool Design Document
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `pages` tool is designed to search HoloViz documentation and return relevant pages based on a user query. This document outlines the architecture, implementation approach, and technical decisions for building this functionality.
|
|
6
|
+
|
|
7
|
+
## Current State Analysis
|
|
8
|
+
|
|
9
|
+
The current `pages` tool in `server.py` is a stub that needs:
|
|
10
|
+
1. A `Page` model definition
|
|
11
|
+
2. Data preparation pipeline
|
|
12
|
+
|
|
13
|
+
### Search Implementation Details
|
|
14
|
+
```python
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from fastmcp import Context
|
|
18
|
+
|
|
19
|
+
class DocumentationIndexer:
|
|
20
|
+
def search_pages(
|
|
21
|
+
self,
|
|
22
|
+
name: str | None = None,
|
|
23
|
+
path: str | None = None,
|
|
24
|
+
query: str | None = None,
|
|
25
|
+
package: str | None = None,
|
|
26
|
+
content: bool = True,
|
|
27
|
+
max_results: int | None = 5,
|
|
28
|
+
) -> List[Page]:
|
|
29
|
+
"""Enhanced search with multiple filtering options and regex support."""
|
|
30
|
+
|
|
31
|
+
# Build ChromaDB where clause for metadata filtering
|
|
32
|
+
where_clause = {}
|
|
33
|
+
if package:
|
|
34
|
+
where_clause["package"] = package
|
|
35
|
+
|
|
36
|
+
# Add filtering to where clause
|
|
37
|
+
if path:
|
|
38
|
+
where_clause["source_path"] = {"$regex": path}
|
|
39
|
+
|
|
40
|
+
if name:
|
|
41
|
+
# Exact filename matching, not regex
|
|
42
|
+
where_clause["name"] = name
|
|
43
|
+
|
|
44
|
+
# Perform search
|
|
45
|
+
if query:
|
|
46
|
+
# Vector similarity search
|
|
47
|
+
results = self.collection.query(
|
|
48
|
+
query_texts=[query],
|
|
49
|
+
n_results=max_results,
|
|
50
|
+
where=where_clause if where_clause else None
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
# Metadata-only search
|
|
54
|
+
results = self.collection.get(
|
|
55
|
+
where=where_clause if where_clause else None,
|
|
56
|
+
limit=max_results
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Convert to Page objects
|
|
60
|
+
pages = []
|
|
61
|
+
if results['ids'] and results['ids'][0]:
|
|
62
|
+
for i, doc_id in enumerate(results['ids'][0]):
|
|
63
|
+
metadata = results['metadatas'][0][i]
|
|
64
|
+
|
|
65
|
+
# Include full content if requested
|
|
66
|
+
if content:
|
|
67
|
+
content_text = results['documents'][0][i] if results['documents'] else ""
|
|
68
|
+
else:
|
|
69
|
+
# Just metadata - no content
|
|
70
|
+
content_text = None
|
|
71
|
+
|
|
72
|
+
page = Page(
|
|
73
|
+
title=metadata.get('title', ''),
|
|
74
|
+
url=metadata.get('url', ''),
|
|
75
|
+
package=metadata.get('package', ''),
|
|
76
|
+
path=metadata.get('path', ''),
|
|
77
|
+
description=metadata.get('description', ''),
|
|
78
|
+
content_preview=content_text,
|
|
79
|
+
relevance_score=1.0 - results['distances'][0][i] if results.get('distances') else None
|
|
80
|
+
)
|
|
81
|
+
pages.append(page)
|
|
82
|
+
|
|
83
|
+
return pages
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Notebook to Markdown Path Mapping
|
|
87
|
+
```python
|
|
88
|
+
def process_notebook_file(self, file_path: Path, package: str, folder_type: str) -> Optional[Dict]:
|
|
89
|
+
"""Process a notebook file and map it to documentation structure."""
|
|
90
|
+
try:
|
|
91
|
+
# Convert notebook to markdown
|
|
92
|
+
markdown_content = self.convert_notebook_to_markdown(file_path)
|
|
93
|
+
if not markdown_content:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Map notebook path to documentation path
|
|
97
|
+
# examples/reference/widgets/Button.ipynb -> reference/widgets/Button.md
|
|
98
|
+
relative_path = file_path.relative_to(self.repos_dir / package)
|
|
99
|
+
|
|
100
|
+
if str(relative_path).startswith('examples/reference/'):
|
|
101
|
+
# Transform examples/reference/widgets/Button.ipynb to reference/widgets/Button.md
|
|
102
|
+
doc_path = str(relative_path).replace('examples/reference/', 'reference/')
|
|
103
|
+
doc_path = doc_path.replace('.ipynb', '.md')
|
|
104
|
+
else:
|
|
105
|
+
# Keep original path but change extension
|
|
106
|
+
doc_path = str(relative_path).replace('.ipynb', '.md')
|
|
107
|
+
|
|
108
|
+
# Extract title and content
|
|
109
|
+
title = file_path.stem.replace('_', ' ').title()
|
|
110
|
+
# ... rest of processing
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
'title': title,
|
|
114
|
+
'url': self._generate_doc_url(package, Path(doc_path), folder_type),
|
|
115
|
+
'package': package,
|
|
116
|
+
'path': doc_path, # This is the key - mapped path
|
|
117
|
+
'description': description,
|
|
118
|
+
'content': text_content,
|
|
119
|
+
'folder_type': folder_type,
|
|
120
|
+
'id': f"{package}_{doc_path}".replace('/', '_').replace('.', '_')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Failed to process notebook file {file_path}: {e}")
|
|
125
|
+
return None
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Configuration Managementor documentation indexing
|
|
129
|
+
3. Vector search implementation
|
|
130
|
+
4. Integration with the FastMCP framework
|
|
131
|
+
|
|
132
|
+
## Architecture
|
|
133
|
+
|
|
134
|
+
### 1. Data Models (`src/holoviz_mcp/docs_mcp/models.py`)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from pydantic import BaseModel, HttpUrl
|
|
138
|
+
from typing import Optional
|
|
139
|
+
|
|
140
|
+
class Page(BaseModel):
|
|
141
|
+
"""Represents a documentation page in the HoloViz ecosystem."""
|
|
142
|
+
|
|
143
|
+
title: str
|
|
144
|
+
url: HttpUrl
|
|
145
|
+
package: str
|
|
146
|
+
path: str
|
|
147
|
+
description: Optional[str] = None
|
|
148
|
+
content_preview: Optional[str] = None
|
|
149
|
+
relevance_score: Optional[float] = None
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 2. Data Preparation Pipeline (`src/holoviz_mcp/docs_mcp/data.py`)
|
|
153
|
+
|
|
154
|
+
The data preparation will involve:
|
|
155
|
+
|
|
156
|
+
#### HoloViz Packages to Index
|
|
157
|
+
- `panel` - Main Panel library
|
|
158
|
+
- `param` - Parameter handling
|
|
159
|
+
- `datashader` - Large data visualization
|
|
160
|
+
- `holoviews` - Declarative data visualization
|
|
161
|
+
- `geoviews` - Geographic visualization
|
|
162
|
+
- `hvplot` - High-level plotting interface
|
|
163
|
+
- `colorcet` - Color palettes
|
|
164
|
+
- `lumen` - Dashboard building
|
|
165
|
+
- `panel-material-ui` - Material UI components
|
|
166
|
+
|
|
167
|
+
#### Implementation Strategy
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import asyncio
|
|
171
|
+
import git
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
from typing import List, Dict, Optional
|
|
174
|
+
import chromadb
|
|
175
|
+
from sentence_transformers import SentenceTransformer
|
|
176
|
+
import markdown
|
|
177
|
+
import yaml
|
|
178
|
+
from nbconvert import MarkdownExporter
|
|
179
|
+
|
|
180
|
+
class DocumentationIndexer:
|
|
181
|
+
"""Handles cloning, processing, and indexing of HoloViz documentation."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, data_dir: Path = Path("~/holoviz_mcp/data").expanduser()):
|
|
184
|
+
self.data_dir = data_dir
|
|
185
|
+
self.chroma_client = chromadb.PersistentClient(path=str(data_dir / "chroma"))
|
|
186
|
+
self.collection = self.chroma_client.get_or_create_collection("holoviz_docs")
|
|
187
|
+
self.model = SentenceTransformer('all-MiniLM-L6-v2')
|
|
188
|
+
self.nb_exporter = MarkdownExporter()
|
|
189
|
+
self.config = self.load_config()
|
|
190
|
+
|
|
191
|
+
def load_config(self) -> Dict:
|
|
192
|
+
"""Load configuration from file specified by environment variable."""
|
|
193
|
+
|
|
194
|
+
def clone_or_update_repos(self, ctx: Context = None):
|
|
195
|
+
"""Clone or update all HoloViz repositories with progress reporting."""
|
|
196
|
+
|
|
197
|
+
def extract_docs_metadata(self, repo_path: Path, package: str) -> List[Dict]:
|
|
198
|
+
"""Extract documentation files and metadata from a repository."""
|
|
199
|
+
|
|
200
|
+
def process_markdown_file(self, file_path: Path, package: str) -> Dict:
|
|
201
|
+
"""Process a single markdown file and extract relevant information."""
|
|
202
|
+
|
|
203
|
+
def process_notebook_file(self, file_path: Path, package: str) -> Dict:
|
|
204
|
+
"""Process a single notebook file and convert to markdown."""
|
|
205
|
+
|
|
206
|
+
def create_embeddings(self, docs: List[Dict], ctx: Context = None) -> List[List[float]]:
|
|
207
|
+
"""Create embeddings for documentation content with progress reporting."""
|
|
208
|
+
|
|
209
|
+
def index_documentation(self, ctx: Context = None):
|
|
210
|
+
"""Main method to index all documentation with progress reporting."""
|
|
211
|
+
|
|
212
|
+
def search_pages(
|
|
213
|
+
self,
|
|
214
|
+
name: str | None = None,
|
|
215
|
+
path: str | None = None,
|
|
216
|
+
query: str | None = None,
|
|
217
|
+
package: str | None = None,
|
|
218
|
+
content: bool = True,
|
|
219
|
+
max_results: int | None = 5,
|
|
220
|
+
) -> List[Page]:
|
|
221
|
+
"""Search indexed documentation and return relevant pages.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
name: Optional exact filename to filter by (e.g., "Audio.md", "Button.md")
|
|
225
|
+
path: Optional path pattern (regex) to filter by (e.g., "reference/.*\.md$")
|
|
226
|
+
query: Optional semantic search query
|
|
227
|
+
package: Optional package name to filter by
|
|
228
|
+
content: Whether to include full content in results (default: True)
|
|
229
|
+
max_results: Maximum number of results to return
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of Page objects matching the search criteria
|
|
233
|
+
"""
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 3. Documentation Processing Strategy
|
|
237
|
+
|
|
238
|
+
#### Git Repository Cloning
|
|
239
|
+
- Clone HoloViz repositories using GitPython
|
|
240
|
+
- Process both `docs/` and `examples/reference/` folders (configurable)
|
|
241
|
+
- Handle both markdown (.md) and notebook (.ipynb) files
|
|
242
|
+
- Support additional repositories via configuration file
|
|
243
|
+
|
|
244
|
+
#### Configuration File Support
|
|
245
|
+
- External configuration file for additional repositories
|
|
246
|
+
- Environment variable `HOLOVIZ_CONFIG_FILE` to specify config location
|
|
247
|
+
- Configurable folder names for documentation and reference guides
|
|
248
|
+
- Default folders: `docs/` and `examples/reference/`
|
|
249
|
+
|
|
250
|
+
#### Notebook Processing
|
|
251
|
+
- Use nbconvert to convert Jupyter notebooks to markdown
|
|
252
|
+
- Extract metadata and content from converted markdown
|
|
253
|
+
- Preserve code examples and outputs in searchable format
|
|
254
|
+
- Process notebooks in configurable reference folders
|
|
255
|
+
- **Important**: Reference notebooks in `examples/reference/**/*.ipynb` are indexed as `.md` files with paths like `reference/widgets/Button.md` to match the published documentation structure
|
|
256
|
+
|
|
257
|
+
#### Content Indexing
|
|
258
|
+
- Use ChromaDB for persistent vector storage
|
|
259
|
+
- Sentence transformers for embedding generation
|
|
260
|
+
- Direct markdown parsing without HTML conversion
|
|
261
|
+
- Metadata extraction including:
|
|
262
|
+
- Package name
|
|
263
|
+
- File path
|
|
264
|
+
- Title (from frontmatter or headings)
|
|
265
|
+
- Description (from frontmatter or first paragraph)
|
|
266
|
+
- Content preview
|
|
267
|
+
|
|
268
|
+
### 4. Search Implementation
|
|
269
|
+
|
|
270
|
+
#### Enhanced Search Strategy
|
|
271
|
+
The search implementation now supports multiple search modes:
|
|
272
|
+
|
|
273
|
+
1. **Name-based Search**: Filter by exact filename matching (e.g., "Audio.md", "Button.md")
|
|
274
|
+
2. **Path-based Search**: Filter by file path using regex patterns (e.g., "reference/.*\.md$")
|
|
275
|
+
3. **Semantic Search**: Vector similarity search using queries
|
|
276
|
+
4. **Package Filtering**: Limit results to specific packages
|
|
277
|
+
5. **Content Control**: Option to include full content (default: True) vs. metadata only
|
|
278
|
+
|
|
279
|
+
#### Regex Pattern Support
|
|
280
|
+
The `path` parameter supports regex patterns for flexible path matching:
|
|
281
|
+
|
|
282
|
+
- `reference/.*\.md$` - All markdown files in reference folder and subfolders
|
|
283
|
+
- `reference/panes/.*\.md$` - All markdown files in reference/panes folder
|
|
284
|
+
- `.*/Audio.*` - All files containing "Audio" anywhere in the tree
|
|
285
|
+
- `docs/how_to/.*` - All files in docs/how_to folder and subfolders
|
|
286
|
+
|
|
287
|
+
#### Search Process
|
|
288
|
+
1. Apply exact filename filtering if `name` parameter provided
|
|
289
|
+
2. Apply path filtering using regex patterns if `path` parameter provided
|
|
290
|
+
3. Apply package filtering if `package` parameter provided
|
|
291
|
+
4. If `query` provided, perform vector similarity search
|
|
292
|
+
5. Combine and rank results by relevance
|
|
293
|
+
6. Return top N results as Page objects
|
|
294
|
+
7. Include full content if `content` is True (default), otherwise metadata only
|
|
295
|
+
|
|
296
|
+
### 5. Integration Points
|
|
297
|
+
|
|
298
|
+
#### Configuration (`src/holoviz_mcp/shared/config.py`)
|
|
299
|
+
```python
|
|
300
|
+
# Add documentation-specific configuration
|
|
301
|
+
DATA_DIR = Path(os.getenv("HOLOVIZ_DATA_DIR", "~/holoviz_mcp/data")).expanduser()
|
|
302
|
+
CONFIG_FILE = os.getenv("HOLOVIZ_CONFIG_FILE", "")
|
|
303
|
+
DOCS_UPDATE_INTERVAL = int(os.getenv("HOLOVIZ_DOCS_UPDATE_INTERVAL", "86400")) # 24 hours
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Configuration File Format
|
|
307
|
+
```yaml
|
|
308
|
+
# holoviz_config.yaml
|
|
309
|
+
repositories:
|
|
310
|
+
# Core HoloViz packages (built-in)
|
|
311
|
+
panel:
|
|
312
|
+
url: "https://github.com/holoviz/panel.git"
|
|
313
|
+
docs_folder: "docs"
|
|
314
|
+
reference_folder: "examples/reference"
|
|
315
|
+
|
|
316
|
+
# Additional user-defined repositories
|
|
317
|
+
my_custom_panel_extension:
|
|
318
|
+
url: "https://github.com/user/my-panel-extension.git"
|
|
319
|
+
docs_folder: "documentation"
|
|
320
|
+
reference_folder: "examples"
|
|
321
|
+
|
|
322
|
+
another_project:
|
|
323
|
+
url: "https://github.com/org/another-project.git"
|
|
324
|
+
docs_folder: "docs"
|
|
325
|
+
reference_folder: "reference"
|
|
326
|
+
|
|
327
|
+
# Default folder configuration
|
|
328
|
+
default_docs_folder: "docs"
|
|
329
|
+
default_reference_folder: "examples/reference"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Index Update Strategy
|
|
333
|
+
|
|
334
|
+
### **First Time Initialization**
|
|
335
|
+
The documentation index will be created **automatically on first use** of either the `get_reference_guide` or `pages` tools:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
# In both tools, there will be logic like:
|
|
339
|
+
if not indexer.is_indexed():
|
|
340
|
+
logger.info("Documentation index not found. Creating initial index...")
|
|
341
|
+
indexer.index_documentation()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Benefits of lazy initialization:**
|
|
345
|
+
- **No upfront setup required** - the index is created when first needed
|
|
346
|
+
- **Better UX** - users can start using tools immediately without waiting for setup
|
|
347
|
+
- **Resource efficiency** - only creates index when actually needed
|
|
348
|
+
- **Simpler deployment** - no background processes or startup delays
|
|
349
|
+
|
|
350
|
+
### **Subsequent Updates**
|
|
351
|
+
After the initial creation, the index will be updated **only when manually triggered** using the `update_index` tool:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# Manual updates only
|
|
355
|
+
update_index() # User must explicitly call this
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Benefits of manual updates:**
|
|
359
|
+
- **Predictable behavior** - index content doesn't change unexpectedly
|
|
360
|
+
- **Resource control** - users control when expensive re-indexing happens
|
|
361
|
+
- **Reliability** - no background processes that could fail silently
|
|
362
|
+
- **Explicit control** - users decide when to refresh documentation
|
|
363
|
+
|
|
364
|
+
### **Implementation Details**
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
class DocumentationIndexer:
|
|
368
|
+
def is_indexed(self) -> bool:
|
|
369
|
+
"""Check if documentation index exists and is valid."""
|
|
370
|
+
try:
|
|
371
|
+
# Check if ChromaDB collection exists and has documents
|
|
372
|
+
collection = self.chroma_client.get_collection("holoviz_docs")
|
|
373
|
+
count = collection.count()
|
|
374
|
+
return count > 0
|
|
375
|
+
except Exception:
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
def ensure_indexed(self, ctx: Context = None):
|
|
379
|
+
"""Ensure documentation is indexed, creating if necessary."""
|
|
380
|
+
if not self.is_indexed():
|
|
381
|
+
await self.log_info("Documentation index not found. Creating initial index...")
|
|
382
|
+
self.index_documentation(ctx)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### **Recommended Usage Pattern**
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# Day 1: First use (automatic index creation)
|
|
389
|
+
get_reference_guide("Button", "panel") # Index created automatically during first call
|
|
390
|
+
|
|
391
|
+
# Day 7: Manual refresh to get latest documentation
|
|
392
|
+
update_index() # User explicitly updates when needed
|
|
393
|
+
|
|
394
|
+
# Day 14: Another manual refresh
|
|
395
|
+
update_index() # User controls update frequency
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### **Alternative Strategies (Future Enhancements)**
|
|
399
|
+
|
|
400
|
+
The following could be added in future versions but are not currently implemented:
|
|
401
|
+
|
|
402
|
+
1. **Time-based updates**: Check for updates every 24 hours (via `DOCS_UPDATE_INTERVAL`)
|
|
403
|
+
2. **Webhook updates**: Update when GitHub repositories change
|
|
404
|
+
3. **Startup checks**: Check for stale index on server startup
|
|
405
|
+
4. **Background updates**: Periodic updates without blocking user requests
|
|
406
|
+
|
|
407
|
+
This strategy balances **ease of use** (automatic first-time setup) with **control** (manual updates when needed), making it suitable for an MCP server where users want predictable, on-demand documentation access.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""[HoloViz](https://holoviz.org/) Documentation MCP Server.
|
|
2
|
+
|
|
3
|
+
This server provides tools, resources and prompts for accessing documentation related to the HoloViz ecosystems.
|
|
4
|
+
|
|
5
|
+
Use this server to search and access documentation for HoloViz libraries, including Panel and hvPlot.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from fastmcp import Context
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from holoviz_mcp.config.loader import get_config
|
|
14
|
+
from holoviz_mcp.holoviz_mcp.data import DocumentationIndexer
|
|
15
|
+
from holoviz_mcp.holoviz_mcp.data import get_best_practices as _get_best_practices
|
|
16
|
+
from holoviz_mcp.holoviz_mcp.data import list_best_practices as _list_best_practices
|
|
17
|
+
from holoviz_mcp.holoviz_mcp.models import Document
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Global indexer instance
|
|
22
|
+
_indexer = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_indexer() -> DocumentationIndexer:
|
|
26
|
+
"""Get or create the global DocumentationIndexer instance."""
|
|
27
|
+
global _indexer
|
|
28
|
+
if _indexer is None:
|
|
29
|
+
_indexer = DocumentationIndexer()
|
|
30
|
+
return _indexer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# The HoloViz MCP server instance
|
|
34
|
+
mcp: FastMCP = FastMCP(
|
|
35
|
+
name="documentation",
|
|
36
|
+
instructions="""
|
|
37
|
+
[HoloViz](https://holoviz.org/) Documentation MCP Server.
|
|
38
|
+
|
|
39
|
+
This server provides tools, resources and prompts for accessing documentation related to the HoloViz ecosystems.
|
|
40
|
+
|
|
41
|
+
Use this server to search and access documentation for HoloViz libraries, including Panel and hvPlot.
|
|
42
|
+
""",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool
|
|
47
|
+
def get_best_practices(project: str) -> str:
|
|
48
|
+
"""Get best practices for using a project with LLMs.
|
|
49
|
+
|
|
50
|
+
DO Always use this tool to get best practices for using a project with LLMs before using it!
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
project (str): The name of the project to get best practices for. For example, "panel", "panel-material-ui", etc.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
str: A string containing the best practices for the project in Markdown format.
|
|
58
|
+
"""
|
|
59
|
+
return _get_best_practices(project)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@mcp.tool
|
|
63
|
+
def list_best_practices() -> list[str]:
|
|
64
|
+
"""List all available best practices projects.
|
|
65
|
+
|
|
66
|
+
This tool discovers available best practices from both user and default directories,
|
|
67
|
+
with user resources taking precedence over default ones.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
list[str]: A list of project names that have best practices available.
|
|
72
|
+
Names are returned in hyphenated format (e.g., "panel-material-ui").
|
|
73
|
+
"""
|
|
74
|
+
return _list_best_practices()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@mcp.tool
|
|
78
|
+
async def get_reference_guide(component: str, project: str | None = None, content: bool = True, ctx: Context | None = None) -> list[Document]:
|
|
79
|
+
"""Find reference guides for specific HoloViz components.
|
|
80
|
+
|
|
81
|
+
Reference guides are a subset of all documents that focus on specific UI components
|
|
82
|
+
or plot types, such as:
|
|
83
|
+
|
|
84
|
+
- `panel`: "Button", "TextInput", ...
|
|
85
|
+
- `hvplot`: "bar", "scatter", ...
|
|
86
|
+
- ...
|
|
87
|
+
|
|
88
|
+
DO use this tool to easily find reference guides for specific components in HoloViz libraries.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
component (str): Name of the component (e.g., "Button", "TextInput", "bar", "scatter")
|
|
92
|
+
project (str, optional): Project name. Defaults to None (searches all projects).
|
|
93
|
+
Options: "panel", "panel-material-ui", "hvplot", "param", "holoviews"
|
|
94
|
+
content (bool, optional): Whether to include full content. Defaults to True.
|
|
95
|
+
Set to False to only return metadata for faster responses.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
list[Document]: A list of reference guides for the component.
|
|
100
|
+
|
|
101
|
+
Examples
|
|
102
|
+
--------
|
|
103
|
+
>>> get_reference_guide("Button") # Find Button component guide across all projects
|
|
104
|
+
>>> get_reference_guide("Button", "panel") # Find Panel Button component guide specifically
|
|
105
|
+
>>> get_reference_guide("TextInput", "panel-material-ui") # Find Material UI TextInput guide
|
|
106
|
+
>>> get_reference_guide("bar", "hvplot") # Find hvplot bar chart reference
|
|
107
|
+
>>> get_reference_guide("scatter", "hvplot") # Find hvplot scatter plot reference
|
|
108
|
+
>>> get_reference_guide("Audio", content=False) # Don't include Markdown content for faster response
|
|
109
|
+
"""
|
|
110
|
+
indexer = get_indexer()
|
|
111
|
+
return await indexer.search_get_reference_guide(component, project, content, ctx=ctx)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@mcp.tool
|
|
115
|
+
async def list_projects() -> list[str]:
|
|
116
|
+
"""List all available projects with documentation.
|
|
117
|
+
|
|
118
|
+
This tool discovers all projects that have documentation available in the index,
|
|
119
|
+
including both core HoloViz libraries and any additional user-defined projects.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
list[str]: A list of project names that have documentation available.
|
|
124
|
+
Names are returned in hyphenated format (e.g., "panel-material-ui").
|
|
125
|
+
"""
|
|
126
|
+
indexer = get_indexer()
|
|
127
|
+
return await indexer.list_projects()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool
|
|
131
|
+
async def get_document(path: str, project: str, ctx: Context) -> Document:
|
|
132
|
+
"""Retrieve a specific document by path and project.
|
|
133
|
+
|
|
134
|
+
Use this tool to look up a specific document within a project.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
path: The relative path to the source document (e.g., "index.md", "how_to/customize.md")
|
|
138
|
+
project: the name of the project (e.g., "panel", "panel-material-ui", "hvplot")
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
The markdown content of the specified document.
|
|
143
|
+
"""
|
|
144
|
+
indexer = get_indexer()
|
|
145
|
+
return await indexer.get_document(path, project, ctx=ctx)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp.tool
|
|
149
|
+
async def search(
|
|
150
|
+
query: str,
|
|
151
|
+
project: str | None = None,
|
|
152
|
+
content: bool = True,
|
|
153
|
+
max_results: int = 5,
|
|
154
|
+
ctx: Context | None = None,
|
|
155
|
+
) -> list[Document]:
|
|
156
|
+
"""Search HoloViz documentation using semantic similarity.
|
|
157
|
+
|
|
158
|
+
Optimized for finding relevant documentation based on natural language queries.
|
|
159
|
+
|
|
160
|
+
DO use this tool to find answers to questions about HoloViz libraries, such as Panel and hvPlot.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
query (str): Search query using natural language.
|
|
164
|
+
For example "How to style Material UI components?" or "interactive plotting with widgets"
|
|
165
|
+
project (str, optional): Optional project filter. Defaults to None.
|
|
166
|
+
Options: "panel", "panel-material-ui", "hvplot", "param", "holoviews"
|
|
167
|
+
content (bool, optional): Whether to include full content. Defaults to True.
|
|
168
|
+
Set to False to only return metadata for faster responses.
|
|
169
|
+
max_results (int, optional): Maximum number of results to return. Defaults to 5.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
list[Document]: A list of relevant documents ordered by relevance.
|
|
174
|
+
|
|
175
|
+
Examples
|
|
176
|
+
--------
|
|
177
|
+
>>> search("How to style Material UI components?", "panel-material-ui") # Semantic search in specific project
|
|
178
|
+
>>> search("interactive plotting with widgets", "hvplot") # Find hvplot interactive guides
|
|
179
|
+
>>> search("dashboard layout best practices") # Search across all projects
|
|
180
|
+
>>> search("custom widgets", project="panel", max_results=3) # Limit results
|
|
181
|
+
>>> search("parameter handling", content=False) # Get metadata only for overview
|
|
182
|
+
"""
|
|
183
|
+
indexer = get_indexer()
|
|
184
|
+
return await indexer.search(query, project, content, max_results, ctx=ctx)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@mcp.tool(enabled=False)
|
|
188
|
+
async def update_index(ctx: Context) -> str:
|
|
189
|
+
"""Update the documentation index by re-cloning repositories and re-indexing content.
|
|
190
|
+
|
|
191
|
+
DO use this tool periodically (weekly) to ensure the documentation index is up-to-date
|
|
192
|
+
with the latest changes in the HoloViz ecosystem.
|
|
193
|
+
|
|
194
|
+
Warning: This operation can take a long time (up to 5 minutes) depending on the number of
|
|
195
|
+
repositories and their size!
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
str: Status message indicating the result of the update operation.
|
|
200
|
+
|
|
201
|
+
Examples
|
|
202
|
+
--------
|
|
203
|
+
>>> update_index() # Updates all documentation repositories and rebuilds index
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
indexer = get_indexer()
|
|
207
|
+
|
|
208
|
+
# Use True as ctx to enable print statements for user feedback
|
|
209
|
+
await indexer.index_documentation(ctx=ctx)
|
|
210
|
+
|
|
211
|
+
return "Documentation index updated successfully."
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error(f"Failed to update documentation index: {e}")
|
|
214
|
+
error_msg = f"Failed to update documentation index: {str(e)}"
|
|
215
|
+
return error_msg
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
config = get_config()
|
|
220
|
+
mcp.run(transport=config.server.transport)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""hvPlot MCP Server."""
|