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
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()
|