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
holoviz_mcp/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ """Command-line interface for HoloViz MCP.
2
+
3
+ This module provides a unified CLI using Typer for all HoloViz MCP commands.
4
+ """
5
+
6
+ import typer
7
+ from typing_extensions import Annotated
8
+
9
+ app = typer.Typer(
10
+ name="holoviz-mcp",
11
+ help="HoloViz Model Context Protocol (MCP) server and utilities.",
12
+ no_args_is_help=False, # Allow running without args to start the server
13
+ )
14
+
15
+
16
+ @app.callback(invoke_without_command=True)
17
+ def main(
18
+ ctx: typer.Context,
19
+ version: Annotated[
20
+ bool,
21
+ typer.Option(
22
+ "--version",
23
+ "-v",
24
+ help="Show version and exit.",
25
+ ),
26
+ ] = False,
27
+ ) -> None:
28
+ """HoloViz MCP server and utilities.
29
+
30
+ Run without arguments to start the MCP server, or use subcommands for other operations.
31
+ """
32
+ # Handle version flag
33
+ if version:
34
+ from holoviz_mcp import __version__
35
+
36
+ typer.echo(f"holoviz-mcp version {__version__}")
37
+ raise typer.Exit()
38
+
39
+ # If no subcommand is invoked, run the default server
40
+ if ctx.invoked_subcommand is None:
41
+ from holoviz_mcp.server import main as server_main
42
+
43
+ server_main()
44
+
45
+
46
+ @app.command()
47
+ def update() -> None:
48
+ """Update the documentation index.
49
+
50
+ This command clones/updates HoloViz repositories and builds the vector database
51
+ for documentation search. First run may take up to 10 minutes.
52
+ """
53
+ from holoviz_mcp.holoviz_mcp.data import main as update_main
54
+
55
+ update_main()
56
+
57
+
58
+ @app.command()
59
+ def serve() -> None:
60
+ """Serve Panel apps from the apps directory.
61
+
62
+ This command starts a Panel server to host all Panel apps found in the apps directory.
63
+ """
64
+ from holoviz_mcp.serve import main as serve_main
65
+
66
+ serve_main()
67
+
68
+
69
+ def cli_main() -> None:
70
+ """Entry point for the CLI."""
71
+ app()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ cli_main()
holoviz_mcp/client.py ADDED
@@ -0,0 +1,94 @@
1
+ """Client for interacting with the HoloViz MCP server.
2
+
3
+ This module provides a programmatic interface for calling tools on the HoloViz MCP server.
4
+ It maintains a singleton client instance to avoid redundant server initialization.
5
+
6
+ Examples
7
+ --------
8
+ >>> from holoviz_mcp.client import call_tool
9
+ >>>
10
+ >>> # List available Panel components
11
+ >>> result = await call_tool("panel_list_components", {})
12
+ >>>
13
+ >>> # Search documentation
14
+ >>> result = await call_tool("holoviz_search", {"query": "Button"})
15
+ """
16
+
17
+ import asyncio
18
+ from typing import Any
19
+
20
+ from fastmcp import Client
21
+ from fastmcp.client.client import CallToolResult
22
+
23
+ from holoviz_mcp.server import mcp
24
+ from holoviz_mcp.server import setup_composed_server
25
+
26
+ __all__ = ["call_tool"]
27
+
28
+ _CLIENT: Client | None = None
29
+ _CLIENT_LOCK = asyncio.Lock()
30
+
31
+
32
+ async def _setup_composed_server() -> None:
33
+ """Set up and cache the composed server.
34
+
35
+ This function ensures the server is properly initialized before creating
36
+ a client. It only needs to be called once and its result is cached.
37
+ """
38
+ await setup_composed_server()
39
+
40
+
41
+ async def _create_client() -> Client:
42
+ """Create a new MCP client connected to the HoloViz MCP server.
43
+
44
+ Returns
45
+ -------
46
+ Client
47
+ A FastMCP client instance connected to the composed HoloViz server.
48
+ """
49
+ await _setup_composed_server()
50
+ return Client(mcp)
51
+
52
+
53
+ async def call_tool(tool_name: str, parameters: dict[str, Any]) -> CallToolResult:
54
+ """Call a tool on the MCP server and return the result.
55
+
56
+ This function maintains a singleton client instance to avoid redundant
57
+ server initialization. The first call will initialize the server and
58
+ create a client; subsequent calls reuse the same client.
59
+
60
+ The client initialization is protected by an asyncio.Lock to prevent
61
+ race conditions when multiple tasks call this function concurrently.
62
+
63
+ Parameters
64
+ ----------
65
+ tool_name : str
66
+ The name of the tool to call (e.g., "panel_list_components",
67
+ "holoviz_search", "hvplot_list_plot_types").
68
+ parameters : dict[str, Any]
69
+ A dictionary of parameters to pass to the tool.
70
+
71
+ Returns
72
+ -------
73
+ CallToolResult
74
+ The result returned by the tool, which contains the tool's output
75
+ and any error information.
76
+
77
+ Examples
78
+ --------
79
+ >>> # List all Panel components
80
+ >>> result = await call_tool("panel_list_components", {})
81
+ >>>
82
+ >>> # Search for a specific component
83
+ >>> result = await call_tool("panel_search", {"query": "Button", "limit": 5})
84
+ >>>
85
+ >>> # Get documentation for a project
86
+ >>> result = await call_tool("holoviz_get_best_practices", {"project": "panel"})
87
+ """
88
+ global _CLIENT
89
+ async with _CLIENT_LOCK:
90
+ if _CLIENT is None:
91
+ _CLIENT = await _create_client()
92
+
93
+ async with _CLIENT:
94
+ return await _CLIENT.call_tool(tool_name, parameters)
@@ -0,0 +1,29 @@
1
+ """Configuration package for HoloViz MCP server."""
2
+
3
+ from .loader import ConfigLoader
4
+ from .loader import ConfigurationError
5
+ from .loader import get_config
6
+ from .loader import get_config_loader
7
+ from .loader import reload_config
8
+ from .models import DocsConfig
9
+ from .models import GitRepository
10
+ from .models import HoloVizMCPConfig
11
+ from .models import PromptConfig
12
+ from .models import ResourceConfig
13
+ from .models import ServerConfig
14
+
15
+ __all__ = [
16
+ # Loader
17
+ "ConfigLoader",
18
+ "ConfigurationError",
19
+ "get_config",
20
+ "get_config_loader",
21
+ "reload_config",
22
+ # Models
23
+ "DocsConfig",
24
+ "GitRepository",
25
+ "HoloVizMCPConfig",
26
+ "PromptConfig",
27
+ "ResourceConfig",
28
+ "ServerConfig",
29
+ ]
@@ -0,0 +1,178 @@
1
+ # Default configuration for HoloViz MCP server
2
+ # This file provides the default settings that are used when no user configuration is provided.
3
+
4
+ server:
5
+ name: holoviz-mcp
6
+ version: "1.0.0"
7
+ description: "Model Context Protocol server for HoloViz ecosystem"
8
+ log_level: INFO
9
+ transport: stdio
10
+ anonymized_telemetry: false
11
+ jupyter_server_proxy_url: ""
12
+
13
+ docs:
14
+ repositories:
15
+ panel:
16
+ url: https://github.com/holoviz/panel.git
17
+ branch: main
18
+ folders:
19
+ doc:
20
+ url_path: ""
21
+ examples/reference:
22
+ url_path: "/reference"
23
+ base_url: https://panel.holoviz.org
24
+ reference_patterns:
25
+ - "examples/reference/**/*.md"
26
+ - "examples/reference/**/*.ipynb"
27
+ - "examples/reference/**/*.rst"
28
+ panel-material-ui:
29
+ url: https://github.com/panel-extensions/panel-material-ui.git
30
+ branch: main
31
+ folders:
32
+ doc:
33
+ url_path: ""
34
+ examples/reference:
35
+ url_path: "/reference"
36
+ base_url: https://panel-material-ui.holoviz.org/
37
+ reference_patterns:
38
+ - "examples/reference/**/*.md"
39
+ - "examples/reference/**/*.ipynb"
40
+ - "examples/reference/**/*.rst"
41
+ hvplot:
42
+ url: https://github.com/holoviz/hvplot.git
43
+ branch: main
44
+ folders:
45
+ doc:
46
+ url_path: ""
47
+ base_url: https://hvplot.holoviz.org
48
+ reference_patterns:
49
+ - "doc/reference/**/*.md"
50
+ - "doc/reference/**/*.ipynb"
51
+ - "doc/reference/**/*.rst"
52
+ param:
53
+ url: https://github.com/holoviz/param.git
54
+ branch: main
55
+ folders:
56
+ doc:
57
+ url_path: ""
58
+ base_url: https://param.holoviz.org
59
+ reference_patterns:
60
+ - "doc/reference/**/*.md"
61
+ - "doc/reference/**/*.ipynb"
62
+ - "doc/reference/**/*.rst"
63
+ holoviews:
64
+ url: https://github.com/holoviz/holoviews.git
65
+ branch: main
66
+ folders:
67
+ doc:
68
+ url_path: ""
69
+ examples:
70
+ url_path: ""
71
+ base_url: https://holoviews.org
72
+ reference_patterns:
73
+ - "examples/reference/**/*.md"
74
+ - "examples/reference/**/*.ipynb"
75
+ - "examples/reference/**/*.rst"
76
+ holoviz:
77
+ url: https://github.com/holoviz/holoviz.git
78
+ branch: main
79
+ folders:
80
+ doc:
81
+ url_path: ""
82
+ examples/tutorial:
83
+ url_path: "/tutorial"
84
+ base_url: https://holoviz.org
85
+ url_transform: "datashader"
86
+ reference_patterns:
87
+ - "doc/reference/**/*.md"
88
+ - "doc/reference/**/*.ipynb"
89
+ - "doc/reference/**/*.rst"
90
+ datashader:
91
+ url: https://github.com/holoviz/datashader.git
92
+ branch: main
93
+ folders:
94
+ doc:
95
+ url_path: ""
96
+ examples:
97
+ url_path: ""
98
+ base_url: https://datashader.org
99
+ url_transform: "datashader"
100
+ reference_patterns:
101
+ - "doc/reference/**/*.md"
102
+ - "doc/reference/**/*.ipynb"
103
+ - "doc/reference/**/*.rst"
104
+ geoviews:
105
+ url: https://github.com/holoviz/geoviews.git
106
+ branch: main
107
+ folders:
108
+ doc:
109
+ url_path: ""
110
+ examples:
111
+ url_path: "/"
112
+ base_url: https://geoviews.org
113
+ reference_patterns:
114
+ - "doc/reference/**/*.md"
115
+ - "doc/reference/**/*.ipynb"
116
+ - "doc/reference/**/*.rst"
117
+ colorcet:
118
+ url: https://github.com/holoviz/colorcet.git
119
+ branch: main
120
+ folders:
121
+ doc:
122
+ url_path: ""
123
+ base_url: https://colorcet.holoviz.org
124
+ reference_patterns:
125
+ - "doc/reference/**/*.md"
126
+ - "doc/reference/**/*.ipynb"
127
+ - "doc/reference/**/*.rst"
128
+ lumen:
129
+ url: https://github.com/holoviz/lumen.git
130
+ branch: main
131
+ folders:
132
+ doc:
133
+ url_path: ""
134
+ base_url: https://lumen.holoviz.org
135
+ reference_patterns:
136
+ - "docs/reference/**/*.md"
137
+ - "docs/reference/**/*.ipynb"
138
+ - "docs/reference/**/*.rst"
139
+ holoviz-mcp:
140
+ url: "https://github.com/MarcSkovMadsen/holoviz-mcp.git"
141
+ url_transform: "plotly"
142
+ folders:
143
+ docs:
144
+ url_path: ""
145
+ base_url: "https://marcskovmadsen.github.io/holoviz-mcp"
146
+ bokeh:
147
+ url: "https://github.com/bokeh/bokeh.git"
148
+ base_url: "https://docs.bokeh.org/en/latest/docs"
149
+ folders:
150
+ docs/bokeh/source/docs:
151
+ url_path: "/"
152
+
153
+ index_patterns:
154
+ - "**/*.md"
155
+ - "**/*.ipynb"
156
+ - "**/*.rst"
157
+
158
+ exclude_patterns:
159
+ - "**/node_modules/**"
160
+ - "**/.git/**"
161
+ - "**/build/**"
162
+ - "**/__pycache__/**"
163
+ - "**/.pytest_cache/**"
164
+
165
+ max_file_size: 1048576 # 1MB
166
+ update_interval: 86400 # 24 hours
167
+
168
+ resources:
169
+ search_paths: [] # Additional search paths for user resources
170
+ # Default search paths are automatically included:
171
+ # - Package default resources (src/holoviz_mcp/config/resources/)
172
+ # - User config resources (~/.holoviz_mcp/config/resources/)
173
+
174
+ prompts:
175
+ search_paths: [] # Additional search paths for user prompts
176
+ # Default search paths are automatically included:
177
+ # - Package default prompts (src/holoviz_mcp/config/prompts/)
178
+ # - User config prompts (~/.holoviz_mcp/config/prompts/)
@@ -0,0 +1,316 @@
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
+ },
117
+ "docs": {
118
+ "repositories": {}, # No more Python-side defaults!
119
+ "index_patterns": ["**/*.md", "**/*.rst", "**/*.txt"],
120
+ "exclude_patterns": ["**/node_modules/**", "**/.git/**", "**/build/**"],
121
+ "max_file_size": 1024 * 1024, # 1MB
122
+ "update_interval": 86400, # 24 hours
123
+ },
124
+ "resources": {"search_paths": []},
125
+ "prompts": {"search_paths": []},
126
+ }
127
+
128
+ def _load_yaml_file(self, file_path: Path) -> dict[str, Any]:
129
+ """Load YAML file safely.
130
+
131
+ Args:
132
+ file_path: Path to YAML file.
133
+
134
+ Returns
135
+ -------
136
+ Parsed YAML content.
137
+
138
+ Raises
139
+ ------
140
+ ConfigurationError: If file cannot be loaded or parsed.
141
+ """
142
+ try:
143
+ with open(file_path, "r", encoding="utf-8") as f:
144
+ content = yaml.safe_load(f)
145
+ if content is None:
146
+ return {}
147
+ if not isinstance(content, dict):
148
+ raise ConfigurationError(f"Configuration file must contain a YAML dictionary: {file_path}")
149
+ return content
150
+ except yaml.YAMLError as e:
151
+ raise ConfigurationError(f"Invalid YAML in {file_path}: {e}") from e
152
+ except Exception as e:
153
+ raise ConfigurationError(f"Failed to load {file_path}: {e}") from e
154
+
155
+ def _merge_configs(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
156
+ """Merge two configuration dictionaries recursively.
157
+
158
+ Args:
159
+ base: Base configuration.
160
+ override: Override configuration.
161
+
162
+ Returns
163
+ -------
164
+ Merged configuration.
165
+ """
166
+ result = base.copy()
167
+
168
+ for key, value in override.items():
169
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
170
+ result[key] = self._merge_configs(result[key], value)
171
+ else:
172
+ result[key] = value
173
+
174
+ return result
175
+
176
+ def _apply_env_overrides(self, config: dict[str, Any]) -> dict[str, Any]:
177
+ """Apply environment variable overrides to configuration.
178
+
179
+ Args:
180
+ config: Configuration dictionary.
181
+
182
+ Returns
183
+ -------
184
+ Configuration with environment overrides applied.
185
+ """
186
+ # Log level override
187
+ if "HOLOVIZ_MCP_LOG_LEVEL" in os.environ:
188
+ config.setdefault("server", {})["log_level"] = os.environ["HOLOVIZ_MCP_LOG_LEVEL"]
189
+
190
+ # Server name override
191
+ if "HOLOVIZ_MCP_SERVER_NAME" in os.environ:
192
+ config.setdefault("server", {})["name"] = os.environ["HOLOVIZ_MCP_SERVER_NAME"]
193
+
194
+ # Transport override
195
+ if "HOLOVIZ_MCP_TRANSPORT" in os.environ:
196
+ config.setdefault("server", {})["transport"] = os.environ["HOLOVIZ_MCP_TRANSPORT"]
197
+
198
+ # Host override (for HTTP transport)
199
+ if "HOLOVIZ_MCP_HOST" in os.environ:
200
+ config.setdefault("server", {})["host"] = os.environ["HOLOVIZ_MCP_HOST"]
201
+
202
+ # Port override (for HTTP transport)
203
+ if "HOLOVIZ_MCP_PORT" in os.environ:
204
+ port_str = os.environ["HOLOVIZ_MCP_PORT"]
205
+ try:
206
+ port = int(port_str)
207
+ if not (1 <= port <= 65535):
208
+ raise ValueError(f"Port must be between 1 and 65535, got {port}")
209
+ config.setdefault("server", {})["port"] = port
210
+ except ValueError as e:
211
+ raise ConfigurationError(f"Invalid HOLOVIZ_MCP_PORT: {port_str}") from e
212
+
213
+ # Telemetry override
214
+ if "ANONYMIZED_TELEMETRY" in os.environ:
215
+ config.setdefault("server", {})["anonymized_telemetry"] = os.environ["ANONYMIZED_TELEMETRY"].lower() in ("true", "1", "yes", "on")
216
+
217
+ # Jupyter proxy URL override
218
+ if "JUPYTER_SERVER_PROXY_URL" in os.environ:
219
+ config.setdefault("server", {})["jupyter_server_proxy_url"] = os.environ["JUPYTER_SERVER_PROXY_URL"]
220
+
221
+ return config
222
+
223
+ def get_repos_dir(self) -> Path:
224
+ """Get the repository download directory."""
225
+ config = self.load_config()
226
+ return config.repos_dir
227
+
228
+ def get_resources_dir(self) -> Path:
229
+ """Get the resources directory."""
230
+ config = self.load_config()
231
+ return config.resources_dir()
232
+
233
+ def get_prompts_dir(self) -> Path:
234
+ """Get the prompts directory."""
235
+ config = self.load_config()
236
+ return config.prompts_dir()
237
+
238
+ def get_best_practices_dir(self) -> Path:
239
+ """Get the best practices directory."""
240
+ config = self.load_config()
241
+ return config.best_practices_dir()
242
+
243
+ def create_default_user_config(self) -> None:
244
+ """Create a default user configuration file."""
245
+ config = self.load_config()
246
+ config_file = config.config_file_path()
247
+
248
+ # Create directories if they don't exist
249
+ config_file.parent.mkdir(parents=True, exist_ok=True)
250
+
251
+ # Don't overwrite existing config
252
+ if config_file.exists():
253
+ logger.info(f"Configuration file already exists: {config_file}")
254
+ return
255
+
256
+ # Create default configuration
257
+ template = {
258
+ "server": {
259
+ "name": "holoviz-mcp",
260
+ "log_level": "INFO",
261
+ },
262
+ "docs": {
263
+ "repositories": {
264
+ "example-repo": {
265
+ "url": "https://github.com/example/repo.git",
266
+ "branch": "main",
267
+ "folders": {"doc": {"url_path": ""}},
268
+ "base_url": "https://example.readthedocs.io",
269
+ "reference_patterns": ["doc/reference/**/*.md", "examples/reference/**/*.ipynb"],
270
+ }
271
+ }
272
+ },
273
+ "resources": {"search_paths": []},
274
+ "prompts": {"search_paths": []},
275
+ }
276
+
277
+ with open(config_file, "w", encoding="utf-8") as f:
278
+ yaml.dump(template, f, default_flow_style=False, sort_keys=False)
279
+
280
+ logger.info(f"Created default user configuration file: {config_file}")
281
+
282
+ def reload_config(self) -> HoloVizMCPConfig:
283
+ """Reload configuration from files.
284
+
285
+ Returns
286
+ -------
287
+ Reloaded configuration.
288
+ """
289
+ self._loaded_config = None
290
+ return self.load_config()
291
+
292
+ def clear_cache(self) -> None:
293
+ """Clear the cached configuration to force reload on next access."""
294
+ self._loaded_config = None
295
+
296
+
297
+ # Global configuration loader instance
298
+ _config_loader: Optional[ConfigLoader] = None
299
+
300
+
301
+ def get_config_loader() -> ConfigLoader:
302
+ """Get the global configuration loader instance."""
303
+ global _config_loader
304
+ if _config_loader is None:
305
+ _config_loader = ConfigLoader()
306
+ return _config_loader
307
+
308
+
309
+ def get_config() -> HoloVizMCPConfig:
310
+ """Get the current configuration."""
311
+ return get_config_loader().load_config()
312
+
313
+
314
+ def reload_config() -> HoloVizMCPConfig:
315
+ """Reload configuration from files."""
316
+ return get_config_loader().reload_config()