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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "uv_build"

[project]
name = "zoo_mcp"
version = "0.12.1"
version = "0.12.2"
requires-python = ">=3.11, <3.14"
dependencies = [
"aiofiles<26.0",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"url": "https://github.com/KittyCAD/zoo-mcp",
"source": "github"
},
"version": "0.12.1",
"version": "0.12.2",
"packages": [
{
"registryType": "pypi",
"identifier": "zoo_mcp",
"version": "0.12.1",
"version": "0.12.2",
"transport": {
"type": "stdio"
},
Expand Down
4 changes: 2 additions & 2 deletions src/zoo_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ def _initialize_kcl_samples() -> None:


# Initialize caches when module is imported
_initialize_kcl_docs()
_initialize_kcl_samples()
# _initialize_kcl_docs()
# _initialize_kcl_samples()
3 changes: 2 additions & 1 deletion src/zoo_mcp/ai_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import time
from pathlib import Path

from kittycad._io_types import SyncUpload
from kittycad.models import (
ApiCallStatus,
FileExportFormat,
Expand Down Expand Up @@ -216,7 +217,7 @@ async def edit_kcl_project(
"No main.kcl file found in the root of the provided project path"
)

file_attachments = {
file_attachments: dict[str, SyncUpload] = {
str(fp.relative_to(proj_path)): str(fp.resolve()) for fp in file_paths
}

Expand Down
180 changes: 1 addition & 179 deletions src/zoo_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,6 @@
from zoo_mcp import ZooMCPException, logger
from zoo_mcp.ai_tools import edit_kcl_project as _edit_kcl_project
from zoo_mcp.ai_tools import text_to_cad as _text_to_cad
from zoo_mcp.kcl_docs import (
get_doc_content,
list_available_docs,
search_docs,
)
from zoo_mcp.kcl_samples import (
SampleData,
get_sample_content,
list_available_samples,
search_samples,
)
from zoo_mcp.utils.image_utils import encode_image, save_image_to_disk
from zoo_mcp.zoo_tools import (
CameraView,
Expand Down Expand Up @@ -377,7 +366,7 @@ async def export_kcl(


@mcp.tool()
async def format_kcl(
def format_kcl(
kcl_code: str | None = None,
kcl_path: str | None = None,
) -> str:
Expand Down Expand Up @@ -800,173 +789,6 @@ async def save_image(
return f"There was an error saving the image: {e}"


@mcp.tool()
async def list_kcl_docs() -> dict | str:
"""List all available KCL documentation topics organized by category.

Returns a dictionary with the following categories:
- kcl-lang: KCL language documentation (syntax, types, functions, etc.)
- kcl-std-functions: Standard library function documentation
- kcl-std-types: Standard library type documentation
- kcl-std-consts: Standard library constants documentation
- kcl-std-modules: Standard library module documentation

Each category contains a list of documentation file paths that can be
retrieved using get_kcl_doc().

Returns:
dict | str: Categories mapped to lists of available documentation paths.
If there was an error, returns an error message string.
"""
logger.info("list_kcl_docs tool called")
try:
return list_available_docs()
except Exception as e:
logger.error("list_kcl_docs tool called with error: %s", e)
return f"There was an error listing KCL documentation: {e}"


@mcp.tool()
async def search_kcl_docs(query: str, max_results: int = 5) -> list[dict] | str:
"""Search KCL documentation by keyword.

Searches across all KCL language and standard library documentation
for the given query. Returns relevant excerpts with surrounding context.

Args:
query (str): The search query (case-insensitive).
max_results (int): Maximum number of results to return (default: 5).

Returns:
list[dict] | str: List of search results, each containing:
- path: The documentation file path
- title: The document title (from first heading)
- excerpt: A relevant excerpt with the match highlighted in context
- match_count: Number of times the query appears in the document
If there was an error, returns an error message string.
"""
logger.info("search_kcl_docs tool called with query: %s", query)
try:
return search_docs(query, max_results)
except Exception as e:
logger.error("search_kcl_docs tool called with error: %s", e)
return f"There was an error searching KCL documentation: {e}"


@mcp.tool()
async def get_kcl_doc(doc_path: str) -> str:
"""Get the full content of a specific KCL documentation file.

Use list_kcl_docs() to see available documentation paths, or
search_kcl_docs() to find relevant documentation by keyword.

Args:
doc_path (str): The path to the documentation file
(e.g., "docs/kcl-lang/functions.md" or "docs/kcl-std/functions/extrude.md")

Returns:
str: The full Markdown content of the documentation file,
or an error message if not found. If there was an error, returns an error message string.
"""
logger.info("get_kcl_doc tool called for path: %s", doc_path)
try:
content = get_doc_content(doc_path)
if content is None:
return f"Documentation not found: {doc_path}. Use list_kcl_docs() to see available paths."
return content
except Exception as e:
logger.error("get_kcl_doc tool called with error: %s", e)
return f"There was an error retrieving KCL documentation: {e}"


@mcp.tool()
async def list_kcl_samples() -> list[dict] | str:
"""List all available KCL sample projects.

Returns a list of all available KCL code samples from the Zoo samples
repository. Each sample demonstrates a specific CAD modeling technique
or creates a particular 3D model.

Returns:
list[dict] | str: List of sample information, each containing:
- name: The sample directory name (use with get_kcl_sample)
- title: Human-readable title
- description: Brief description of what the sample creates
- multipleFiles: Whether the sample contains multiple KCL files
If there was an error, returns an error message string.
"""
logger.info("list_kcl_samples tool called")
try:
return list_available_samples()
except Exception as e:
logger.error("list_kcl_samples tool called with error: %s", e)
return f"There was an error listing KCL samples: {e}"


@mcp.tool()
async def search_kcl_samples(query: str, max_results: int = 5) -> list[dict] | str:
"""Search KCL samples by keyword.

Searches across all KCL sample titles and descriptions
for the given query. Returns matching samples ranked by relevance.

Args:
query (str): The search query (case-insensitive).
max_results (int): Maximum number of results to return (default: 5).

Returns:
list[dict] | str: List of search results, each containing:
- name: The sample directory name (use with get_kcl_sample)
- title: Human-readable title
- description: Brief description of the sample
- multipleFiles: Whether the sample contains multiple KCL files
- match_count: Number of times the query appears in title/description
- excerpt: A relevant excerpt with the match in context
If there was an error, returns an error message string.
"""
logger.info("search_kcl_samples tool called with query: %s", query)
try:
return search_samples(query, max_results)
except Exception as e:
logger.error("search_kcl_samples tool called with error: %s", e)
return f"There was an error searching KCL samples: {e}"


@mcp.tool()
async def get_kcl_sample(sample_name: str) -> SampleData | str:
"""Get the full content of a specific KCL sample including all files.

Retrieves all KCL files that make up a sample project. Some samples
consist of a single main.kcl file, while others have multiple files
(e.g., parameters.kcl, components, etc.).

Use list_kcl_samples() to see available sample names, or
search_kcl_samples() to find samples by keyword.

Args:
sample_name (str): The sample directory name
(e.g., "ball-bearing", "axial-fan", "gear")

Returns:
SampleData | str: A SampleData dictionary containing:
- name: The sample directory name
- title: Human-readable title
- description: Brief description
- multipleFiles: Whether the sample contains multiple files
- files: List of SampleFile dictionaries, each with 'filename' and 'content'
Returns an error message string if the sample is not found. If there was an error, returns an error message string.
"""
logger.info("get_kcl_sample tool called for sample: %s", sample_name)
try:
sample = await get_sample_content(sample_name)
if sample is None:
return f"Sample not found: {sample_name}. Use list_kcl_samples() to see available samples."
return sample
except Exception as e:
logger.error("get_kcl_sample tool called with error: %s", e)
return f"There was an error retrieving KCL sample: {e}"


def main():
logger.info("Starting MCP server...")
mcp.run(transport="stdio")
Expand Down
78 changes: 57 additions & 21 deletions src/zoo_mcp/zoo_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,32 +211,60 @@ class KCLExportFormat(Enum):

class CameraView(Enum):
views = {
"front": {"up": [0, 0, 1], "vantage": [0, -1, 0], "center": [0, 0, 0]},
"back": {"up": [0, 0, 1], "vantage": [0, 1, 0], "center": [0, 0, 0]},
"left": {"up": [0, 0, 1], "vantage": [-1, 0, 0], "center": [0, 0, 0]},
"right": {"up": [0, 0, 1], "vantage": [1, 0, 0], "center": [0, 0, 0]},
"top": {"up": [0, 1, 0], "vantage": [0, 0, 1], "center": [0, 0, 0]},
"bottom": {"up": [0, -1, 0], "vantage": [0, 0, -1], "center": [0, 0, 0]},
"isometric": {"up": [0, 0, 1], "vantage": [1, -1, 1], "center": [0, 0, 0]},
"front": {
"up": [0.0, 0.0, 1.0],
"vantage": [0.0, -1.0, 0.0],
"center": [0.0, 0.0, 0.0],
},
"back": {
"up": [0.0, 0.0, 1.0],
"vantage": [0.0, 1.0, 0.0],
"center": [0.0, 0.0, 0.0],
},
"left": {
"up": [0.0, 0.0, 1.0],
"vantage": [-1.0, 0.0, 0.0],
"center": [0.0, 0.0, 0.0],
},
"right": {
"up": [0.0, 0.0, 1.0],
"vantage": [1.0, 0.0, 0.0],
"center": [0.0, 0.0, 0.0],
},
"top": {
"up": [0.0, 1.0, 0.0],
"vantage": [0.0, 0.0, 1.0],
"center": [0.0, 0.0, 0.0],
},
"bottom": {
"up": [0.0, -1.0, 0.0],
"vantage": [0.0, 0.0, -1.0],
"center": [0.0, 0.0, 0.0],
},
"isometric": {
"up": [0.0, 0.0, 1.0],
"vantage": [1.0, -1.0, 1.0],
"center": [0.0, 0.0, 0.0],
},
"isometric_front_right": {
"up": [0, 0, 1],
"vantage": [1, -1, 1],
"center": [0, 0, 0],
"up": [0.0, 0.0, 1.0],
"vantage": [1.0, -1.0, 1.0],
"center": [0.0, 0.0, 0.0],
},
"isometric_front_left": {
"up": [0, 0, 1],
"vantage": [-1, -1, 1],
"center": [0, 0, 0],
"up": [0.0, 0.0, 1.0],
"vantage": [-1.0, -1.0, 1.0],
"center": [0.0, 0.0, 0.0],
},
"isometric_back_right": {
"up": [0, 0, 1],
"vantage": [1, 1, -1],
"center": [0, 0, 0],
"up": [0.0, 0.0, 1.0],
"vantage": [1.0, 1.0, -1.0],
"center": [0.0, 0.0, 0.0],
},
"isometric_back_left": {
"up": [0, 0, 1],
"vantage": [-1, 1, -1],
"center": [0, 0, 0],
"up": [0.0, 0.0, 1.0],
"vantage": [-1.0, 1.0, -1.0],
"center": [0.0, 0.0, 0.0],
},
}

Expand All @@ -261,7 +289,9 @@ def to_kcl_camera(view: dict[str, list[float]]) -> kcl.CameraLookAt:
)

@staticmethod
def to_kittycad_camera(view: dict[str, list[float]]) -> OptionDefaultCameraLookAt:
def to_kittycad_camera(
view: dict[str, list[float]],
) -> OptionDefaultCameraLookAt:
return OptionDefaultCameraLookAt(
up=Point3d(
x=view["up"][0],
Expand Down Expand Up @@ -1038,7 +1068,13 @@ def zoo_format_kcl(
formatted_code = kcl.format(kcl_code)
return formatted_code
else:
kcl.format_dir(str(kcl_path))
path = Path(kcl_path) # type: ignore[arg-type]
if path.is_file():
code = path.read_text()
formatted = kcl.format(code)
path.write_text(formatted)
else:
kcl.format_dir(str(kcl_path)) # type: ignore[unused-awaitable]
return None
except Exception as e:
logger.error(e)
Expand Down
41 changes: 0 additions & 41 deletions tests/test_docs.py

This file was deleted.

Loading
Loading