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,308 @@
|
|
|
1
|
+
"""Configuration loader for HoloViz MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from .models import HoloVizMCPConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigurationError(Exception):
|
|
20
|
+
"""Raised when configuration cannot be loaded or is invalid."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigLoader:
|
|
24
|
+
"""Loads and manages HoloViz MCP configuration."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: Optional[HoloVizMCPConfig] = None):
|
|
27
|
+
"""Initialize configuration loader.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Pre-configured HoloVizMCPConfig with environment paths.
|
|
31
|
+
If None, loads paths from environment. Configuration will
|
|
32
|
+
still be loaded from files even if this is provided.
|
|
33
|
+
"""
|
|
34
|
+
self._env_config = config
|
|
35
|
+
self._loaded_config: Optional[HoloVizMCPConfig] = None
|
|
36
|
+
|
|
37
|
+
def load_config(self) -> HoloVizMCPConfig:
|
|
38
|
+
"""Load configuration from files and environment.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
Loaded configuration.
|
|
43
|
+
|
|
44
|
+
Raises
|
|
45
|
+
------
|
|
46
|
+
ConfigurationError: If configuration cannot be loaded or is invalid.
|
|
47
|
+
"""
|
|
48
|
+
if self._loaded_config is not None:
|
|
49
|
+
return self._loaded_config
|
|
50
|
+
|
|
51
|
+
# Get environment config (from parameter or environment)
|
|
52
|
+
if self._env_config is not None:
|
|
53
|
+
env_config = self._env_config
|
|
54
|
+
else:
|
|
55
|
+
env_config = HoloVizMCPConfig()
|
|
56
|
+
|
|
57
|
+
# Start with default configuration dict
|
|
58
|
+
config_dict = self._get_default_config()
|
|
59
|
+
|
|
60
|
+
# Load from default config file if it exists
|
|
61
|
+
default_config_file = env_config.default_dir / "config.yaml"
|
|
62
|
+
if default_config_file.exists():
|
|
63
|
+
try:
|
|
64
|
+
default_config = self._load_yaml_file(default_config_file)
|
|
65
|
+
config_dict = self._merge_configs(config_dict, default_config)
|
|
66
|
+
logger.info(f"Loaded default configuration from {default_config_file}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning(f"Failed to load default config from {default_config_file}: {e}")
|
|
69
|
+
|
|
70
|
+
# Load from user config file if it exists
|
|
71
|
+
user_config_file = env_config.config_file_path()
|
|
72
|
+
if user_config_file.exists():
|
|
73
|
+
user_config = self._load_yaml_file(user_config_file)
|
|
74
|
+
# Filter out any unknown fields to prevent validation errors
|
|
75
|
+
user_config = self._filter_known_fields(user_config)
|
|
76
|
+
config_dict = self._merge_configs(config_dict, user_config)
|
|
77
|
+
logger.info(f"Loaded user configuration from {user_config_file}")
|
|
78
|
+
|
|
79
|
+
# Apply environment variable overrides
|
|
80
|
+
config_dict = self._apply_env_overrides(config_dict)
|
|
81
|
+
|
|
82
|
+
# Add the environment paths to the config dict
|
|
83
|
+
config_dict.update(
|
|
84
|
+
{
|
|
85
|
+
"user_dir": env_config.user_dir,
|
|
86
|
+
"default_dir": env_config.default_dir,
|
|
87
|
+
"repos_dir": env_config.repos_dir,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Create the final configuration
|
|
92
|
+
try:
|
|
93
|
+
self._loaded_config = HoloVizMCPConfig(**config_dict)
|
|
94
|
+
except ValidationError as e:
|
|
95
|
+
raise ConfigurationError(f"Invalid configuration: {e}") from e
|
|
96
|
+
|
|
97
|
+
return self._loaded_config
|
|
98
|
+
|
|
99
|
+
def _filter_known_fields(self, config_dict: dict[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""Filter out unknown fields that aren't part of the HoloVizMCPConfig schema.
|
|
101
|
+
|
|
102
|
+
This prevents validation errors when loading user config files that might
|
|
103
|
+
contain extra fields.
|
|
104
|
+
"""
|
|
105
|
+
known_fields = {"server", "docs", "resources", "prompts", "user_dir", "default_dir", "repos_dir"}
|
|
106
|
+
return {k: v for k, v in config_dict.items() if k in known_fields}
|
|
107
|
+
|
|
108
|
+
def _get_default_config(self) -> dict[str, Any]:
|
|
109
|
+
"""Get default configuration dictionary."""
|
|
110
|
+
return {
|
|
111
|
+
"server": {
|
|
112
|
+
"name": "holoviz-mcp",
|
|
113
|
+
"version": "1.0.0",
|
|
114
|
+
"description": "Model Context Protocol server for HoloViz ecosystem",
|
|
115
|
+
"log_level": "INFO",
|
|
116
|
+
"security": {"allow_code_execution": True},
|
|
117
|
+
},
|
|
118
|
+
"docs": {
|
|
119
|
+
"repositories": {}, # No more Python-side defaults!
|
|
120
|
+
"index_patterns": ["**/*.md", "**/*.rst", "**/*.txt"],
|
|
121
|
+
"exclude_patterns": ["**/node_modules/**", "**/.git/**", "**/build/**"],
|
|
122
|
+
"max_file_size": 1024 * 1024, # 1MB
|
|
123
|
+
"update_interval": 86400, # 24 hours
|
|
124
|
+
},
|
|
125
|
+
"resources": {"search_paths": []},
|
|
126
|
+
"prompts": {"search_paths": []},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def _load_yaml_file(self, file_path: Path) -> dict[str, Any]:
|
|
130
|
+
"""Load YAML file safely.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
file_path: Path to YAML file.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Parsed YAML content.
|
|
138
|
+
|
|
139
|
+
Raises
|
|
140
|
+
------
|
|
141
|
+
ConfigurationError: If file cannot be loaded or parsed.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
145
|
+
content = yaml.safe_load(f)
|
|
146
|
+
if content is None:
|
|
147
|
+
return {}
|
|
148
|
+
if not isinstance(content, dict):
|
|
149
|
+
raise ConfigurationError(f"Configuration file must contain a YAML dictionary: {file_path}")
|
|
150
|
+
return content
|
|
151
|
+
except yaml.YAMLError as e:
|
|
152
|
+
raise ConfigurationError(f"Invalid YAML in {file_path}: {e}") from e
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise ConfigurationError(f"Failed to load {file_path}: {e}") from e
|
|
155
|
+
|
|
156
|
+
def _merge_configs(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
157
|
+
"""Merge two configuration dictionaries recursively.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
base: Base configuration.
|
|
161
|
+
override: Override configuration.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
Merged configuration.
|
|
166
|
+
"""
|
|
167
|
+
result = base.copy()
|
|
168
|
+
|
|
169
|
+
for key, value in override.items():
|
|
170
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
171
|
+
result[key] = self._merge_configs(result[key], value)
|
|
172
|
+
else:
|
|
173
|
+
result[key] = value
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
def _apply_env_overrides(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
+
"""Apply environment variable overrides to configuration.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
config: Configuration dictionary.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
Configuration with environment overrides applied.
|
|
186
|
+
"""
|
|
187
|
+
# Log level override
|
|
188
|
+
if "HOLOVIZ_MCP_LOG_LEVEL" in os.environ:
|
|
189
|
+
config.setdefault("server", {})["log_level"] = os.environ["HOLOVIZ_MCP_LOG_LEVEL"]
|
|
190
|
+
|
|
191
|
+
# Server name override
|
|
192
|
+
if "HOLOVIZ_MCP_SERVER_NAME" in os.environ:
|
|
193
|
+
config.setdefault("server", {})["name"] = os.environ["HOLOVIZ_MCP_SERVER_NAME"]
|
|
194
|
+
|
|
195
|
+
# Transport override
|
|
196
|
+
if "HOLOVIZ_MCP_TRANSPORT" in os.environ:
|
|
197
|
+
config.setdefault("server", {})["transport"] = os.environ["HOLOVIZ_MCP_TRANSPORT"]
|
|
198
|
+
|
|
199
|
+
# Telemetry override
|
|
200
|
+
if "ANONYMIZED_TELEMETRY" in os.environ:
|
|
201
|
+
config.setdefault("server", {})["anonymized_telemetry"] = os.environ["ANONYMIZED_TELEMETRY"].lower() in ("true", "1", "yes", "on")
|
|
202
|
+
|
|
203
|
+
# Jupyter proxy URL override
|
|
204
|
+
if "JUPYTER_SERVER_PROXY_URL" in os.environ:
|
|
205
|
+
config.setdefault("server", {})["jupyter_server_proxy_url"] = os.environ["JUPYTER_SERVER_PROXY_URL"]
|
|
206
|
+
|
|
207
|
+
# Security configuration override
|
|
208
|
+
if "HOLOVIZ_MCP_ALLOW_CODE_EXECUTION" in os.environ:
|
|
209
|
+
config.setdefault("server", {}).setdefault("security", {})["allow_code_execution"] = os.environ["HOLOVIZ_MCP_ALLOW_CODE_EXECUTION"].lower() in (
|
|
210
|
+
"true",
|
|
211
|
+
"1",
|
|
212
|
+
"yes",
|
|
213
|
+
"on",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return config
|
|
217
|
+
|
|
218
|
+
def get_repos_dir(self) -> Path:
|
|
219
|
+
"""Get the repository download directory."""
|
|
220
|
+
config = self.load_config()
|
|
221
|
+
return config.repos_dir
|
|
222
|
+
|
|
223
|
+
def get_resources_dir(self) -> Path:
|
|
224
|
+
"""Get the resources directory."""
|
|
225
|
+
config = self.load_config()
|
|
226
|
+
return config.resources_dir()
|
|
227
|
+
|
|
228
|
+
def get_prompts_dir(self) -> Path:
|
|
229
|
+
"""Get the prompts directory."""
|
|
230
|
+
config = self.load_config()
|
|
231
|
+
return config.prompts_dir()
|
|
232
|
+
|
|
233
|
+
def get_best_practices_dir(self) -> Path:
|
|
234
|
+
"""Get the best practices directory."""
|
|
235
|
+
config = self.load_config()
|
|
236
|
+
return config.best_practices_dir()
|
|
237
|
+
|
|
238
|
+
def create_default_user_config(self) -> None:
|
|
239
|
+
"""Create a default user configuration file."""
|
|
240
|
+
config = self.load_config()
|
|
241
|
+
config_file = config.config_file_path()
|
|
242
|
+
|
|
243
|
+
# Create directories if they don't exist
|
|
244
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
|
|
246
|
+
# Don't overwrite existing config
|
|
247
|
+
if config_file.exists():
|
|
248
|
+
logger.info(f"Configuration file already exists: {config_file}")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Create default configuration
|
|
252
|
+
template = {
|
|
253
|
+
"server": {"name": "holoviz-mcp", "log_level": "INFO", "security": {"allow_code_execution": True}},
|
|
254
|
+
"docs": {
|
|
255
|
+
"repositories": {
|
|
256
|
+
"example-repo": {
|
|
257
|
+
"url": "https://github.com/example/repo.git",
|
|
258
|
+
"branch": "main",
|
|
259
|
+
"folders": {"doc": {"url_path": ""}},
|
|
260
|
+
"base_url": "https://example.readthedocs.io",
|
|
261
|
+
"reference_patterns": ["doc/reference/**/*.md", "examples/reference/**/*.ipynb"],
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"resources": {"search_paths": []},
|
|
266
|
+
"prompts": {"search_paths": []},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
270
|
+
yaml.dump(template, f, default_flow_style=False, sort_keys=False)
|
|
271
|
+
|
|
272
|
+
logger.info(f"Created default user configuration file: {config_file}")
|
|
273
|
+
|
|
274
|
+
def reload_config(self) -> HoloVizMCPConfig:
|
|
275
|
+
"""Reload configuration from files.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
Reloaded configuration.
|
|
280
|
+
"""
|
|
281
|
+
self._loaded_config = None
|
|
282
|
+
return self.load_config()
|
|
283
|
+
|
|
284
|
+
def clear_cache(self) -> None:
|
|
285
|
+
"""Clear the cached configuration to force reload on next access."""
|
|
286
|
+
self._loaded_config = None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Global configuration loader instance
|
|
290
|
+
_config_loader: Optional[ConfigLoader] = None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_config_loader() -> ConfigLoader:
|
|
294
|
+
"""Get the global configuration loader instance."""
|
|
295
|
+
global _config_loader
|
|
296
|
+
if _config_loader is None:
|
|
297
|
+
_config_loader = ConfigLoader()
|
|
298
|
+
return _config_loader
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_config() -> HoloVizMCPConfig:
|
|
302
|
+
"""Get the current configuration."""
|
|
303
|
+
return get_config_loader().load_config()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def reload_config() -> HoloVizMCPConfig:
|
|
307
|
+
"""Reload configuration from files."""
|
|
308
|
+
return get_config_loader().reload_config()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Configuration models for HoloViz MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from pydantic import AnyHttpUrl
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
from pydantic import ConfigDict
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
from pydantic import PositiveInt
|
|
16
|
+
from pydantic import field_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _holoviz_mcp_user_dir() -> Path:
|
|
20
|
+
"""Get the default user directory for HoloViz MCP."""
|
|
21
|
+
return Path(os.environ.get("HOLOVIZ_MCP_USER_DIR", Path.home() / ".holoviz-mcp"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _holoviz_mcp_default_dir() -> Path:
|
|
25
|
+
"""Get the default configuration directory for HoloViz MCP."""
|
|
26
|
+
return Path(os.environ.get("HOLOVIZ_MCP_DEFAULT_DIR", Path(__file__).parent.parent / "config"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FolderConfig(BaseModel):
|
|
30
|
+
"""Configuration for a folder within a repository."""
|
|
31
|
+
|
|
32
|
+
url_path: str = Field(default="", description="URL path mapping for this folder (e.g., '' for root, '/reference' for reference docs)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitRepository(BaseModel):
|
|
36
|
+
"""Configuration for a Git repository."""
|
|
37
|
+
|
|
38
|
+
url: AnyHttpUrl = Field(..., description="Git repository URL")
|
|
39
|
+
branch: Optional[str] = Field(default=None, description="Git branch to use")
|
|
40
|
+
tag: Optional[str] = Field(default=None, description="Git tag to use (e.g., '1.7.2')")
|
|
41
|
+
commit: Optional[str] = Field(default=None, description="Git commit hash to use")
|
|
42
|
+
folders: Union[list[str], dict[str, FolderConfig]] = Field(
|
|
43
|
+
default_factory=lambda: {"doc": FolderConfig()},
|
|
44
|
+
description="Folders to index within the repository. Can be a list of folder names or a dict mapping folder names to FolderConfig objects",
|
|
45
|
+
)
|
|
46
|
+
base_url: AnyHttpUrl = Field(..., description="Base URL for documentation links")
|
|
47
|
+
url_transform: Literal["holoviz", "plotly", "datashader"] = Field(
|
|
48
|
+
default="holoviz",
|
|
49
|
+
description="""How to transform file path into URL:
|
|
50
|
+
|
|
51
|
+
- holoViz transform suffix to .html: filename.md -> filename.html
|
|
52
|
+
- plotly transform suffix to /: filename.md -> filename/
|
|
53
|
+
- datashader removes leading index and transform suffix to .html: 01_filename.md -> filename.html
|
|
54
|
+
""",
|
|
55
|
+
)
|
|
56
|
+
reference_patterns: list[str] = Field(
|
|
57
|
+
default_factory=lambda: ["examples/reference/**/*.md", "examples/reference/**/*.ipynb"], description="Glob patterns for reference documentation files"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@field_validator("tag")
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate_tag(cls, v):
|
|
63
|
+
"""Validate git tag format, allowing both 'v1.2.3' and '1.2.3' formats."""
|
|
64
|
+
if v is not None and v.startswith("v"):
|
|
65
|
+
# Allow tags like 'v1.7.2' but also suggest plain version numbers
|
|
66
|
+
pass
|
|
67
|
+
return v
|
|
68
|
+
|
|
69
|
+
@field_validator("folders")
|
|
70
|
+
@classmethod
|
|
71
|
+
def validate_folders(cls, v):
|
|
72
|
+
"""Validate and normalize folders configuration."""
|
|
73
|
+
if isinstance(v, list):
|
|
74
|
+
# Convert list to dict format for backward compatibility
|
|
75
|
+
return {folder: FolderConfig() for folder in v}
|
|
76
|
+
elif isinstance(v, dict):
|
|
77
|
+
# Ensure all values are FolderConfig objects
|
|
78
|
+
result = {}
|
|
79
|
+
for folder, config in v.items():
|
|
80
|
+
if isinstance(config, dict):
|
|
81
|
+
result[folder] = FolderConfig(**config)
|
|
82
|
+
elif isinstance(config, FolderConfig):
|
|
83
|
+
result[folder] = config
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Invalid folder config for '{folder}': must be dict or FolderConfig")
|
|
86
|
+
return result
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError("folders must be a list or dict")
|
|
89
|
+
|
|
90
|
+
def get_folder_names(self) -> list[str]:
|
|
91
|
+
"""Get list of folder names for backward compatibility."""
|
|
92
|
+
if isinstance(self.folders, dict):
|
|
93
|
+
return list(self.folders.keys())
|
|
94
|
+
return self.folders
|
|
95
|
+
|
|
96
|
+
def get_folder_url_path(self, folder_name: str) -> str:
|
|
97
|
+
"""Get URL path for a specific folder."""
|
|
98
|
+
if isinstance(self.folders, dict):
|
|
99
|
+
folder_config = self.folders.get(folder_name)
|
|
100
|
+
if folder_config:
|
|
101
|
+
return folder_config.url_path
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DocsConfig(BaseModel):
|
|
106
|
+
"""Configuration for documentation repositories."""
|
|
107
|
+
|
|
108
|
+
repositories: dict[str, GitRepository] = Field(default_factory=dict, description="Dictionary mapping package names to repository configurations")
|
|
109
|
+
index_patterns: list[str] = Field(
|
|
110
|
+
default_factory=lambda: ["**/*.md", "**/*.rst", "**/*.txt"], description="File patterns to include when indexing documentation"
|
|
111
|
+
)
|
|
112
|
+
exclude_patterns: list[str] = Field(
|
|
113
|
+
default_factory=lambda: ["**/node_modules/**", "**/.git/**", "**/build/**"], description="File patterns to exclude when indexing documentation"
|
|
114
|
+
)
|
|
115
|
+
max_file_size: PositiveInt = Field(
|
|
116
|
+
default=1024 * 1024, # 1MB
|
|
117
|
+
description="Maximum file size in bytes to index",
|
|
118
|
+
)
|
|
119
|
+
update_interval: PositiveInt = Field(
|
|
120
|
+
default=86400, # 24 hours
|
|
121
|
+
description="How often to check for updates in seconds",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ResourceConfig(BaseModel):
|
|
126
|
+
"""Configuration for resources (best practices, etc.)."""
|
|
127
|
+
|
|
128
|
+
search_paths: list[Path] = Field(default_factory=list, description="Additional paths to search for resources")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PromptConfig(BaseModel):
|
|
132
|
+
"""Configuration for prompts."""
|
|
133
|
+
|
|
134
|
+
search_paths: list[Path] = Field(default_factory=list, description="Additional paths to search for prompts")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class SecurityConfig(BaseModel):
|
|
138
|
+
"""Security configuration for code execution."""
|
|
139
|
+
|
|
140
|
+
allow_code_execution: bool = Field(
|
|
141
|
+
default=True,
|
|
142
|
+
description="Allow LLM to execute arbitrary code. " "This can be dangerous and should only be enabled in trusted environments.",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ServerConfig(BaseModel):
|
|
147
|
+
"""Configuration for the MCP server."""
|
|
148
|
+
|
|
149
|
+
name: str = Field(default="holoviz-mcp", description="Server name")
|
|
150
|
+
version: str = Field(default="1.0.0", description="Server version")
|
|
151
|
+
description: str = Field(default="Model Context Protocol server for HoloViz ecosystem", description="Server description")
|
|
152
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO", description="Logging level")
|
|
153
|
+
transport: Literal["stdio", "http"] = Field(default="stdio", description="Transport protocol for MCP communication")
|
|
154
|
+
anonymized_telemetry: bool = Field(default=False, description="Enable anonymized telemetry")
|
|
155
|
+
jupyter_server_proxy_url: str = Field(default="", description="Jupyter server proxy URL for Panel app integration")
|
|
156
|
+
security: SecurityConfig = Field(default_factory=SecurityConfig, description="Security configuration")
|
|
157
|
+
vector_db_path: Path = Field(
|
|
158
|
+
default_factory=lambda: (_holoviz_mcp_user_dir() / "vector_db" / "chroma").expanduser(), description="Path to the Chroma vector database."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class HoloVizMCPConfig(BaseModel):
|
|
163
|
+
"""Main configuration for HoloViz MCP server."""
|
|
164
|
+
|
|
165
|
+
server: ServerConfig = Field(default_factory=ServerConfig)
|
|
166
|
+
docs: DocsConfig = Field(default_factory=DocsConfig)
|
|
167
|
+
resources: ResourceConfig = Field(default_factory=ResourceConfig)
|
|
168
|
+
prompts: PromptConfig = Field(default_factory=PromptConfig)
|
|
169
|
+
|
|
170
|
+
# Environment paths - merged from EnvironmentConfig with defaults
|
|
171
|
+
user_dir: Path = Field(default_factory=_holoviz_mcp_user_dir, description="User configuration directory")
|
|
172
|
+
default_dir: Path = Field(default_factory=_holoviz_mcp_default_dir, description="Default configuration directory")
|
|
173
|
+
repos_dir: Path = Field(default_factory=lambda: _holoviz_mcp_user_dir() / "repos", description="Repository download directory")
|
|
174
|
+
|
|
175
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
176
|
+
|
|
177
|
+
def config_file_path(self, location: Literal["user", "default"] = "user") -> Path:
|
|
178
|
+
"""Get the path to the configuration file.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
location: Whether to get user or default config file path
|
|
182
|
+
"""
|
|
183
|
+
if location == "user":
|
|
184
|
+
return self.user_dir / "config.yaml"
|
|
185
|
+
else:
|
|
186
|
+
return self.default_dir / "config.yaml"
|
|
187
|
+
|
|
188
|
+
def resources_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
189
|
+
"""Get the path to the resources directory.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
location: Whether to get user or default resources directory
|
|
193
|
+
"""
|
|
194
|
+
if location == "user":
|
|
195
|
+
return self.user_dir / "config" / "resources"
|
|
196
|
+
else:
|
|
197
|
+
return self.default_dir / "resources"
|
|
198
|
+
|
|
199
|
+
def prompts_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
200
|
+
"""Get the path to the prompts directory.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
location: Whether to get user or default prompts directory
|
|
204
|
+
"""
|
|
205
|
+
if location == "user":
|
|
206
|
+
return self.user_dir / "config" / "prompts"
|
|
207
|
+
else:
|
|
208
|
+
return self.default_dir / "prompts"
|
|
209
|
+
|
|
210
|
+
def best_practices_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
211
|
+
"""Get the path to the best practices directory.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
location: Whether to get user or default best practices directory
|
|
215
|
+
"""
|
|
216
|
+
return self.resources_dir(location) / "best-practices"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# hvPlot
|
|
2
|
+
|
|
3
|
+
## General Guidelines
|
|
4
|
+
|
|
5
|
+
- Always import hvplot for your data backend:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import hvplot.pandas # will add .hvplot namespace to Pandas dataframes
|
|
9
|
+
import hvplot.polars # will add .hvplot namespace to Polars dataframes
|
|
10
|
+
...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Prefer Bokeh > Plotly > Matplotlib plotting backend for interactivity
|
|
14
|
+
- DO use bar charts over pie Charts. Pie charts are not supported.
|
|
15
|
+
- DO use NumeralTickFormatter and 'a' formatter for axis formatting:
|
|
16
|
+
|
|
17
|
+
| Input | Format String | Output |
|
|
18
|
+
| - | - | - |
|
|
19
|
+
| 1230974 | '0.0a' | 1.2m |
|
|
20
|
+
| 1460 | '0 a' | 1 k |
|
|
21
|
+
| -104000 | '0a' | -104k |
|
|
22
|
+
|
|
23
|
+
## Developing
|
|
24
|
+
|
|
25
|
+
When developing a hvplot please serve it for development using Panel:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import hvplot.pandas # noqa
|
|
30
|
+
import panel as pn
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
np.random.seed(42)
|
|
35
|
+
dates = pd.date_range("2022-08-01", periods=30, freq="B")
|
|
36
|
+
open_prices = np.cumsum(np.random.normal(100, 2, size=len(dates)))
|
|
37
|
+
high_prices = open_prices + np.random.uniform(1, 5, size=len(dates))
|
|
38
|
+
low_prices = open_prices - np.random.uniform(1, 5, size=len(dates))
|
|
39
|
+
close_prices = open_prices + np.random.uniform(-3, 3, size=len(dates))
|
|
40
|
+
|
|
41
|
+
data = pd.DataFrame({
|
|
42
|
+
"open": open_prices.round(2),
|
|
43
|
+
"high": high_prices.round(2),
|
|
44
|
+
"low": low_prices.round(2),
|
|
45
|
+
"close": close_prices.round(2),
|
|
46
|
+
}, index=dates)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Create a scatter plot of date vs close price
|
|
50
|
+
scatter_plot = data.hvplot.scatter(x="index", y="close", grid=True, title="Close Price Scatter Plot", xlabel="Date", ylabel="Close Price")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Create a Panel app
|
|
54
|
+
app = pn.Column("# Close Price Scatter Plot", scatter_plot)
|
|
55
|
+
|
|
56
|
+
if pn.state.served:
|
|
57
|
+
app.servable()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
panel serve plot.py --dev
|
|
62
|
+
```
|