Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/holoviz_mcp/holoviews_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)


@mcp.tool
@mcp.tool()
async def list_elements(ctx: Context) -> list[str]:
"""
List all available HoloViews visualization elements.
Expand All @@ -50,7 +50,7 @@ async def list_elements(ctx: Context) -> list[str]:
return sorted(elements_list)


@mcp.tool
@mcp.tool()
async def get_docstring(ctx: Context, element: str, backend: Literal["matplotlib", "bokeh", "plotly"] = "bokeh") -> str:
"""
Get the hvPlot docstring for a specific plot type, including available options and usage details.
Expand Down
60 changes: 43 additions & 17 deletions src/holoviz_mcp/holoviz_mcp/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,25 +1258,51 @@ def _clone_or_update_repo_sync(self, repo_name: str, repo_config: "GitRepository

try:
if repo_path.exists():
logger.info(f"Updating {repo_name} repository at {repo_path}...")
repo = git.Repo(repo_path)
repo.remotes.origin.pull()
else:
logger.info(f"Cloning {repo_name} repository to {repo_path}...")
clone_kwargs: dict[str, Any] = {"depth": 1}

if repo_config.branch:
clone_kwargs["branch"] = repo_config.branch
elif repo_config.tag:
clone_kwargs["branch"] = repo_config.tag
elif repo_config.commit:
git.Repo.clone_from(str(repo_config.url), repo_path, **clone_kwargs)
repo = git.Repo(repo_path)
repo.git.checkout(repo_config.commit)

if repo_config.tag:
# Tag checkouts leave the repo in detached HEAD; verify via tag→commit mapping.
matching_tag = next((t for t in repo.tags if t.name == repo_config.tag), None)
if matching_tag and matching_tag.commit == repo.head.commit:
# Already on the correct tag — tags are immutable, nothing to pull.
return repo_path
logger.info(f"Stale checkout for {repo_name}: expected tag '{repo_config.tag}'. Deleting and re-cloning...")
shutil.rmtree(repo_path)
# Fall through to clone below
elif repo_config.branch:
try:
current_branch = repo.active_branch.name
except TypeError:
current_branch = None # Detached HEAD

if current_branch != repo_config.branch:
logger.info(f"Stale checkout for {repo_name}: on '{current_branch}', expected '{repo_config.branch}'. Deleting and re-cloning...")
shutil.rmtree(repo_path)
# Fall through to clone below
else:
logger.info(f"Updating {repo_name} repository at {repo_path}...")
repo.remotes.origin.pull()
return repo_path
else:
logger.info(f"Updating {repo_name} repository at {repo_path}...")
repo.remotes.origin.pull()
return repo_path

# Repo does not exist (fresh or was just deleted above)
logger.info(f"Cloning {repo_name} repository to {repo_path}...")
clone_kwargs: dict[str, Any] = {"depth": 1}

if repo_config.branch:
clone_kwargs["branch"] = repo_config.branch
elif repo_config.tag:
clone_kwargs["branch"] = repo_config.tag
elif repo_config.commit:
git.Repo.clone_from(str(repo_config.url), repo_path, **clone_kwargs)
repo = git.Repo(repo_path)
repo.git.checkout(repo_config.commit)
return repo_path

git.Repo.clone_from(str(repo_config.url), repo_path, **clone_kwargs)
return repo_path
except Exception as e:
logger.warning(f"Failed to clone/update {repo_name}: {e}")
Expand Down Expand Up @@ -1343,7 +1369,7 @@ def _extract_docs_from_repo_sync(self, repo_path: Path, project: str, old_hashes
reference_count = sum(1 for doc in docs if doc["is_reference"])
regular_count = len(docs) - reference_count
if skipped_count:
logger.info(f" {project}: {len(docs)} changed + {skipped_count} unchanged " f"({regular_count} regular, {reference_count} reference guides)")
logger.info(f" {project}: {len(docs)} changed + {skipped_count} unchanged ({regular_count} regular, {reference_count} reference guides)")
else:
logger.info(f" {project}: {len(docs)} total documents ({regular_count} regular, {reference_count} reference guides)")
return {"docs": docs, "skipped_hashes": skipped_hashes}
Expand Down Expand Up @@ -1676,7 +1702,7 @@ async def index_documentation(

clone_extract_elapsed = time.monotonic() - overall_t0
await log_info(
f"Clone + extract completed in {clone_extract_elapsed:.1f}s " f"({len(all_docs)} to index, {len(all_skipped_hashes)} unchanged)",
f"Clone + extract completed in {clone_extract_elapsed:.1f}s ({len(all_docs)} to index, {len(all_skipped_hashes)} unchanged)",
ctx,
)

Expand All @@ -1702,7 +1728,7 @@ async def index_documentation(

docs_to_index = all_docs # all are new or changed (unchanged were skipped by workers)
await log_info(
f"Incremental: {len(new_docs)} new, {len(changed_docs)} changed, " f"{len(all_skipped_hashes)} unchanged, {len(deleted_doc_ids)} deleted",
f"Incremental: {len(new_docs)} new, {len(changed_docs)} changed, {len(all_skipped_hashes)} unchanged, {len(deleted_doc_ids)} deleted",
ctx,
)

Expand Down
16 changes: 8 additions & 8 deletions src/holoviz_mcp/holoviz_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ async def app_lifespan(server: FastMCP):
)


@mcp.tool
@mcp.tool()
def get_skill(name: str) -> str:
"""Get the specified skill for usage with LLMs.

Expand All @@ -181,7 +181,7 @@ def get_skill(name: str) -> str:
return _get_skill(name)


@mcp.tool
@mcp.tool()
def list_skills() -> list[str]:
"""List all available skills.

Expand All @@ -195,7 +195,7 @@ def list_skills() -> list[str]:
return _list_skills()


@mcp.tool
@mcp.tool()
async def get_reference_guide(
component: str,
project: str | None = None,
Expand Down Expand Up @@ -237,7 +237,7 @@ async def get_reference_guide(
return await indexer.search_get_reference_guide(component, project, content, ctx=ctx)


@mcp.tool
@mcp.tool()
async def list_projects() -> list[str]:
"""List all HoloViz and user-defined projects with indexed documentation.

Expand All @@ -253,7 +253,7 @@ async def list_projects() -> list[str]:
return await indexer.list_projects()


@mcp.tool
@mcp.tool()
async def get_document(path: str, project: str, ctx: Context) -> Document:
"""Retrieve a specific document by path and project.

Expand All @@ -271,7 +271,7 @@ async def get_document(path: str, project: str, ctx: Context) -> Document:
return await indexer.get_document(path, project, ctx=ctx)


@mcp.tool
@mcp.tool()
async def search(
query: str,
project: str | None = None,
Expand Down Expand Up @@ -349,7 +349,7 @@ async def search(
return await indexer.search(query, project, content, max_results, max_content_chars, ctx=ctx)


@mcp.tool(enabled=False)
# @mcp.tool(enabled=False)
async def update_index(ctx: Context) -> str:
"""Update the documentation index by re-cloning repositories and re-indexing content.

Expand Down Expand Up @@ -380,7 +380,7 @@ async def update_index(ctx: Context) -> str:
return error_msg


@mcp.tool
@mcp.tool()
async def display(
code: str,
name: str = "",
Expand Down
6 changes: 3 additions & 3 deletions src/holoviz_mcp/hvplot_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _help(
return doc, sig


@mcp.tool
@mcp.tool()
async def list_plot_types(ctx: Context) -> list[str]:
"""
List all available hvPlot plot types supported in the current environment.
Expand Down Expand Up @@ -75,7 +75,7 @@ async def list_plot_types(ctx: Context) -> list[str]:
return sorted(HoloViewsConverter._kind_mapping)


@mcp.tool
@mcp.tool()
async def get_docstring(
ctx: Context, plot_type: str, docstring: bool = True, generic: bool = True, style: Union[Literal["matplotlib", "bokeh", "plotly"], bool] = True
) -> str:
Expand Down Expand Up @@ -111,7 +111,7 @@ async def get_docstring(
return doc


@mcp.tool
@mcp.tool()
async def get_signature(ctx: Context, plot_type: str, style: Union[Literal["matplotlib", "bokeh", "plotly"], bool] = True) -> str:
"""
Get the function signature for a specific hvPlot plot type.
Expand Down
10 changes: 5 additions & 5 deletions src/holoviz_mcp/panel_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async def _get_all_components(ctx: Context) -> list[ComponentDetails]:
return COMPONENTS


@mcp.tool
@mcp.tool()
async def list_packages(ctx: Context) -> list[str]:
"""
List all installed packages that provide Panel UI components.
Expand Down Expand Up @@ -158,7 +158,7 @@ async def list_packages(ctx: Context) -> list[str]:
return sorted(set(component.package for component in await _get_all_components(ctx)))


@mcp.tool
@mcp.tool()
async def search_components(ctx: Context, query: str, package: str | None = None, limit: int = 10) -> list[ComponentSummarySearchResult]:
"""
Search for Panel components by search query and optional package filter.
Expand Down Expand Up @@ -265,7 +265,7 @@ async def _get_component(ctx: Context, name: str | None = None, module_path: str
return components_list


@mcp.tool
@mcp.tool()
async def list_components(ctx: Context, name: str | None = None, module_path: str | None = None, package: str | None = None) -> list[ComponentSummary]:
"""
Get a summary list of Panel components without detailed docstring and parameter information.
Expand Down Expand Up @@ -322,7 +322,7 @@ async def list_components(ctx: Context, name: str | None = None, module_path: st
return components_list


@mcp.tool
@mcp.tool()
async def get_component(ctx: Context, name: str | None = None, module_path: str | None = None, package: str | None = None) -> ComponentDetails:
"""
Get complete details about a single Panel component including docstring and parameters.
Expand Down Expand Up @@ -383,7 +383,7 @@ async def get_component(ctx: Context, name: str | None = None, module_path: str
return component


@mcp.tool
@mcp.tool()
async def get_component_parameters(ctx: Context, name: str | None = None, module_path: str | None = None, package: str | None = None) -> dict[str, ParameterInfo]:
"""
Get detailed parameter information for a single Panel component.
Expand Down
Loading
Loading