Skip to content

Commit 214855e

Browse files
add user configuration
1 parent ea1cb43 commit 214855e

20 files changed

Lines changed: 491 additions & 382 deletions

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,48 @@ Install additional packages and restart the server to include them.
354354

355355
</details>
356356

357+
## ⚙️ User Configuration
358+
359+
HoloViz MCP supports user configuration via a YAML file, allowing you to customize server behavior and documentation sources to fit your workflow.
360+
361+
### Custom Configuration File
362+
363+
You can provide a custom configuration file to override or extend the default settings. By default, configuration is loaded from `~/.holoviz-mcp/config.yaml`, but you can specify a different location using the `HOLOVIZ_MCP_USER_DIR` environment variable:
364+
365+
```bash
366+
export HOLOVIZ_MCP_USER_DIR=/path/to/your/config_dir
367+
```
368+
369+
### Adding Custom Documentation Repositories
370+
371+
To add documentation from other libraries or your own projects, edit your configuration YAML and add entries under `docs.repositories`.
372+
373+
#### Example: Adding Plotly and Altair Documentation
374+
375+
```yaml
376+
docs:
377+
repositories:
378+
plotly:
379+
url: "https://github.com/plotly/plotly.py.git"
380+
branch: "main"
381+
folders:
382+
docs: {}
383+
base_url: "https://plotly.com/python"
384+
altair:
385+
url: "https://github.com/altair-viz/altair.git"
386+
branch: "main"
387+
folders:
388+
doc: {}
389+
base_url: "https://altair-viz.github.io"
390+
```
391+
392+
After updating your configuration:
393+
394+
- update your index (`holoviz-mcp-update`)
395+
- restart the MCP server
396+
397+
Your custom documentation repositories will now be available for search and reference within HoloViz MCP.
398+
357399
## 🔧 Troubleshooting
358400

359401
### Common Issues

src/holoviz_mcp/config/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from .loader import get_config_loader
77
from .loader import reload_config
88
from .models import DocsConfig
9-
from .models import EnvironmentConfig
109
from .models import GitRepository
1110
from .models import HoloVizMCPConfig
1211
from .models import PromptConfig
@@ -23,7 +22,6 @@
2322
"reload_config",
2423
# Models
2524
"DocsConfig",
26-
"EnvironmentConfig",
2725
"GitRepository",
2826
"HoloVizMCPConfig",
2927
"PromptConfig",

src/holoviz_mcp/config/loader.py

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import yaml
1212
from pydantic import ValidationError
1313

14-
from .models import EnvironmentConfig
1514
from .models import HoloVizMCPConfig
1615

1716
logger = logging.getLogger(__name__)
@@ -24,14 +23,16 @@ class ConfigurationError(Exception):
2423
class ConfigLoader:
2524
"""Loads and manages HoloViz MCP configuration."""
2625

27-
def __init__(self, env_config: Optional[EnvironmentConfig] = None):
26+
def __init__(self, config: Optional[HoloVizMCPConfig] = None):
2827
"""Initialize configuration loader.
2928
3029
Args:
31-
env_config: Environment configuration. If None, loads from environment.
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.
3233
"""
33-
self.env_config = env_config or EnvironmentConfig.from_environment()
34-
self._config: Optional[HoloVizMCPConfig] = None
34+
self._env_config = config
35+
self._loaded_config: Optional[HoloVizMCPConfig] = None
3536

3637
def load_config(self) -> HoloVizMCPConfig:
3738
"""Load configuration from files and environment.
@@ -44,14 +45,20 @@ def load_config(self) -> HoloVizMCPConfig:
4445
------
4546
ConfigurationError: If configuration cannot be loaded or is invalid.
4647
"""
47-
if self._config is not None:
48-
return self._config
48+
if self._loaded_config is not None:
49+
return self._loaded_config
4950

50-
# Start with default configuration
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.from_environment()
56+
57+
# Start with default configuration dict
5158
config_dict = self._get_default_config()
5259

5360
# Load from default config file if it exists
54-
default_config_file = self.env_config.default_dir / "config.yaml"
61+
default_config_file = env_config.default_dir / "config.yaml"
5562
if default_config_file.exists():
5663
try:
5764
default_config = self._load_yaml_file(default_config_file)
@@ -61,22 +68,42 @@ def load_config(self) -> HoloVizMCPConfig:
6168
logger.warning(f"Failed to load default config from {default_config_file}: {e}")
6269

6370
# Load from user config file if it exists
64-
user_config_file = self.env_config.config_file_path()
71+
user_config_file = env_config.config_file_path()
6572
if user_config_file.exists():
6673
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)
6776
config_dict = self._merge_configs(config_dict, user_config)
6877
logger.info(f"Loaded user configuration from {user_config_file}")
6978

7079
# Apply environment variable overrides
7180
config_dict = self._apply_env_overrides(config_dict)
7281

73-
# Validate and create configuration
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
7492
try:
75-
self._config = HoloVizMCPConfig(**config_dict)
93+
self._loaded_config = HoloVizMCPConfig(**config_dict)
7694
except ValidationError as e:
7795
raise ConfigurationError(f"Invalid configuration: {e}") from e
7896

79-
return self._config
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}
80107

81108
def _get_default_config(self) -> dict[str, Any]:
82109
"""Get default configuration dictionary."""
@@ -190,23 +217,28 @@ def _apply_env_overrides(self, config: dict[str, Any]) -> dict[str, Any]:
190217

191218
def get_repos_dir(self) -> Path:
192219
"""Get the repository download directory."""
193-
return self.env_config.repos_dir
220+
config = self.load_config()
221+
return config.repos_dir
194222

195223
def get_resources_dir(self) -> Path:
196224
"""Get the resources directory."""
197-
return self.env_config.resources_dir()
225+
config = self.load_config()
226+
return config.resources_dir()
198227

199228
def get_prompts_dir(self) -> Path:
200229
"""Get the prompts directory."""
201-
return self.env_config.prompts_dir()
230+
config = self.load_config()
231+
return config.prompts_dir()
202232

203233
def get_best_practices_dir(self) -> Path:
204234
"""Get the best practices directory."""
205-
return self.env_config.best_practices_dir()
235+
config = self.load_config()
236+
return config.best_practices_dir()
206237

207238
def create_default_user_config(self) -> None:
208239
"""Create a default user configuration file."""
209-
config_file = self.env_config.config_file_path()
240+
config = self.load_config()
241+
config_file = config.config_file_path()
210242

211243
# Create directories if they don't exist
212244
config_file.parent.mkdir(parents=True, exist_ok=True)
@@ -246,9 +278,13 @@ def reload_config(self) -> HoloVizMCPConfig:
246278
-------
247279
Reloaded configuration.
248280
"""
249-
self._config = None
281+
self._loaded_config = None
250282
return self.load_config()
251283

284+
def clear_cache(self) -> None:
285+
"""Clear the cached configuration to force reload on next access."""
286+
self._loaded_config = None
287+
252288

253289
# Global configuration loader instance
254290
_config_loader: Optional[ConfigLoader] = None

src/holoviz_mcp/config/models.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,21 @@ class ServerConfig(BaseModel):
136136
anonymized_telemetry: bool = Field(default=False, description="Enable anonymized telemetry")
137137
jupyter_server_proxy_url: str = Field(default="", description="Jupyter server proxy URL for Panel app integration")
138138
security: SecurityConfig = Field(default_factory=SecurityConfig, description="Security configuration")
139-
vector_db_path: Optional[Path] = Field(default=None, description="Path to the Chroma vector database. If not set, defaults to <user_dir>/vector_db/chroma.")
139+
vector_db_path: Path = Field(default_factory=lambda: Path.home() / ".holoviz-mcp" / "vector_db" / "chroma", description="Path to the Chroma vector database.")
140140

141-
def get_vector_db_path(self, user_dir: Path) -> Path:
142-
"""Return the resolved path to the Chroma vector database.
141+
def resolve_vector_db_path(self, user_dir: Path) -> Path:
142+
"""Resolve the path to the Chroma vector database.
143143
144-
If vector_db_path is set, use it (expanduser). Otherwise, default to <user_dir>/vector_db/chroma.
144+
This method ensures the vector_db_path is properly expanded.
145+
146+
Args:
147+
user_dir: User directory (for compatibility, but not used since vector_db_path is now always set)
148+
149+
Returns
150+
-------
151+
Resolved path to the vector database
145152
"""
146-
if self.vector_db_path:
147-
return Path(self.vector_db_path).expanduser()
148-
return user_dir / "vector_db" / "chroma"
153+
return Path(self.vector_db_path).expanduser()
149154

150155

151156
class HoloVizMCPConfig(BaseModel):
@@ -156,26 +161,29 @@ class HoloVizMCPConfig(BaseModel):
156161
resources: ResourceConfig = Field(default_factory=ResourceConfig)
157162
prompts: PromptConfig = Field(default_factory=PromptConfig)
158163

159-
model_config = ConfigDict(extra="forbid", validate_assignment=True)
160-
164+
# Environment paths - merged from EnvironmentConfig with defaults
165+
user_dir: Path = Field(default_factory=lambda: Path.home() / ".holoviz-mcp", description="User configuration directory")
166+
default_dir: Path = Field(default_factory=lambda: Path(__file__).parent.parent / "config", description="Default configuration directory")
167+
repos_dir: Path = Field(default_factory=lambda: Path.home() / ".holoviz-mcp" / "repos", description="Repository download directory")
161168

162-
class EnvironmentConfig(BaseModel):
163-
"""Environment-based configuration paths."""
164-
165-
user_dir: Path = Field(description="User configuration directory")
166-
default_dir: Path = Field(description="Default configuration directory")
167-
repos_dir: Path = Field(description="Repository download directory")
169+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
168170

169171
@classmethod
170-
def from_environment(cls) -> EnvironmentConfig:
171-
"""Create configuration from environment variables."""
172-
user_dir = Path(os.environ.get("HOLOVIZ_MCP_USER_DIR", Path.home() / ".config" / "holoviz-mcp"))
173-
172+
def from_environment(cls) -> HoloVizMCPConfig:
173+
"""Create configuration from environment variables with defaults."""
174+
user_dir = Path(os.environ.get("HOLOVIZ_MCP_USER_DIR", Path.home() / ".holoviz-mcp"))
174175
default_dir = Path(os.environ.get("HOLOVIZ_MCP_DEFAULT_DIR", Path(__file__).parent.parent / "config"))
175-
176176
repos_dir = Path(os.environ.get("HOLOVIZ_MCP_REPOS_DIR", user_dir / "repos"))
177177

178-
return cls(user_dir=user_dir, default_dir=default_dir, repos_dir=repos_dir)
178+
return cls(
179+
server=ServerConfig(),
180+
docs=DocsConfig(),
181+
resources=ResourceConfig(),
182+
prompts=PromptConfig(),
183+
user_dir=user_dir,
184+
default_dir=default_dir,
185+
repos_dir=repos_dir,
186+
)
179187

180188
def config_file_path(self, location: Literal["user", "default"] = "user") -> Path:
181189
"""Get the path to the configuration file.

src/holoviz_mcp/config/resources/best-practices/panel.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ filtered = filtered[
262262

263263
- DO arrange vertically when displaying `CheckButtonGroup` in a sidebar `CheckButtonGroup(..., vertical=True)`.
264264

265+
### Tabulator
266+
267+
- DO set `Tabulator.disabled=True` unless you would like the user to be able to edit the table.
268+
265269
## Plotting
266270

267271
- DO use bar charts over pie Charts.

0 commit comments

Comments
 (0)