Adjust arcade docs command to the new docs repo structure (#592)

- Updates the `arcade docs` templates, dir/file paths, and URL paths to
reflect the new docs repo structure
- References "MCP Server" instead of "toolkit"
- Auto-detects when it's a Starter MCP server and adds the corresponding
warning in the main doc page
- Fixes a bug that generated the wrong file path to the Python & JS
examples when the package name had an underscore character)
- Introduces some minor improvements, such as pulling the MCP Server
description for `ToolInfo` from the package `pyproject.toml`, instead of
a standard description varying only the MCP Server name

---------

Co-authored-by: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
This commit is contained in:
Renato Byrro 2025-10-02 01:03:24 -03:00 committed by GitHub
parent 56507d7112
commit 7dd62fcc89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 220 additions and 72 deletions

View file

@ -7,11 +7,12 @@ from arcade_cli.toolkit_docs.docs_builder import (
build_example_path, build_example_path,
build_examples, build_examples,
build_toolkit_mdx, build_toolkit_mdx,
build_toolkit_mdx_path, build_toolkit_mdx_file_path,
) )
from arcade_cli.toolkit_docs.utils import ( from arcade_cli.toolkit_docs.utils import (
get_all_enumerations, get_all_enumerations,
get_list_of_tools, get_list_of_tools,
has_wrapper_tools_directory,
print_debug_func, print_debug_func,
read_toolkit_metadata, read_toolkit_metadata,
resolve_api_key, resolve_api_key,
@ -44,8 +45,9 @@ def generate_toolkit_docs(
docs_dir = standardize_dir_path(docs_dir) docs_dir = standardize_dir_path(docs_dir)
toolkit_dir = standardize_dir_path(toolkit_dir) toolkit_dir = standardize_dir_path(toolkit_dir)
is_wrapper_toolkit = has_wrapper_tools_directory(toolkit_dir)
print_debug("Reading toolkit metadata") print_debug("Reading server metadata")
pip_package_name = read_toolkit_metadata(toolkit_dir) pip_package_name = read_toolkit_metadata(toolkit_dir)
print_debug(f"Getting list of tools for {toolkit_name} from the local Python environment") print_debug(f"Getting list of tools for {toolkit_name} from the local Python environment")
@ -56,16 +58,19 @@ def generate_toolkit_docs(
print_debug("Getting all enumerations potentially used in tool argument specs") print_debug("Getting all enumerations potentially used in tool argument specs")
enums = get_all_enumerations(toolkit_dir) enums = get_all_enumerations(toolkit_dir)
print_debug(f"Building /{toolkit_name.lower()}.mdx file") toolkit_mdx_file_path = build_toolkit_mdx_file_path(docs_section, docs_dir, toolkit_name)
print_debug(f"Building {toolkit_mdx_file_path} file")
toolkit_mdx = build_toolkit_mdx( toolkit_mdx = build_toolkit_mdx(
toolkit_package_name=toolkit_name,
toolkit_dir=toolkit_dir,
tools=tools, tools=tools,
docs_section=docs_section, docs_section=docs_section,
enums=enums, enums=enums,
pip_package_name=pip_package_name, pip_package_name=pip_package_name,
openai_model=openai_model, openai_model=openai_model,
is_wrapper_toolkit=is_wrapper_toolkit,
) )
toolkit_mdx_path = build_toolkit_mdx_path(docs_section, docs_dir, toolkit_name) write_file(toolkit_mdx_file_path, toolkit_mdx)
write_file(toolkit_mdx_path, toolkit_mdx)
if tool_call_examples: if tool_call_examples:
print_debug("Building tool-call examples in Python and JavaScript") print_debug("Building tool-call examples in Python and JavaScript")

View file

@ -19,6 +19,8 @@ from arcade_cli.toolkit_docs.templates import (
ENUM_MDX, ENUM_MDX,
ENUM_VALUE, ENUM_VALUE,
GENERIC_PROVIDER_CONFIG, GENERIC_PROVIDER_CONFIG,
STARTER_TOOL_INFO_CALL,
STARTER_TOOLKIT_HEADER_IMPORT,
TABBED_EXAMPLES_LIST, TABBED_EXAMPLES_LIST,
TABLE_OF_CONTENTS, TABLE_OF_CONTENTS,
TABLE_OF_CONTENTS_ITEM, TABLE_OF_CONTENTS_ITEM,
@ -36,6 +38,8 @@ from arcade_cli.toolkit_docs.templates import (
from arcade_cli.toolkit_docs.utils import ( from arcade_cli.toolkit_docs.utils import (
clean_fully_qualified_name, clean_fully_qualified_name,
find_enum_by_options, find_enum_by_options,
find_pyproject_toml,
get_pyproject_description,
get_toolkit_auth_type, get_toolkit_auth_type,
is_well_known_provider, is_well_known_provider,
pascal_to_snake_case, pascal_to_snake_case,
@ -44,15 +48,30 @@ from arcade_cli.toolkit_docs.utils import (
console = Console() console = Console()
def build_toolkit_mdx_path(docs_section: str, docs_root_dir: str, toolkit_name: str) -> str: def build_toolkit_mdx_dir_path(
return os.path.join( docs_section: str,
docs_root_dir: str,
toolkit_name: str,
ensure_exists: bool = True,
) -> str:
dir_path = os.path.join(
docs_root_dir, docs_root_dir,
"pages", "app",
"toolkits", "en",
"mcp-servers",
docs_section, docs_section,
f"{toolkit_name.lower()}.mdx", f"{toolkit_name.lower().replace('_', '-')}",
) )
if ensure_exists:
os.makedirs(dir_path, exist_ok=True)
return dir_path
def build_toolkit_mdx_file_path(docs_section: str, docs_root_dir: str, toolkit_name: str) -> str:
toolkit_dir_path = build_toolkit_mdx_dir_path(docs_section, docs_root_dir, toolkit_name)
return os.path.join(toolkit_dir_path, "page.mdx")
def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name: str) -> str: def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name: str) -> str:
return os.path.join( return os.path.join(
@ -60,13 +79,15 @@ def build_example_path(example_filename: str, docs_root_dir: str, toolkit_name:
"public", "public",
"examples", "examples",
"integrations", "integrations",
"toolkits", "mcp-servers",
toolkit_name.lower(), toolkit_name.lower(),
example_filename, example_filename,
) )
def build_toolkit_mdx( def build_toolkit_mdx(
toolkit_package_name: str,
toolkit_dir: str,
tools: list[ToolDefinition], tools: list[ToolDefinition],
docs_section: str, docs_section: str,
enums: dict[str, type[Enum]], enums: dict[str, type[Enum]],
@ -74,14 +95,32 @@ def build_toolkit_mdx(
openai_model: str, openai_model: str,
toolkit_header_template: str = TOOLKIT_HEADER, toolkit_header_template: str = TOOLKIT_HEADER,
toolkit_page_template: str = TOOLKIT_PAGE, toolkit_page_template: str = TOOLKIT_PAGE,
is_wrapper_toolkit: bool = False,
) -> tuple[str, str]: ) -> tuple[str, str]:
sample_tool = tools[0] sample_tool = tools[0]
toolkit_name = sample_tool.toolkit.name toolkit_name = sample_tool.toolkit.name
toolkit_version = sample_tool.toolkit.version toolkit_version = sample_tool.toolkit.version
auth_type = get_toolkit_auth_type(sample_tool.requirements.authorization) auth_type = get_toolkit_auth_type(sample_tool.requirements)
if is_wrapper_toolkit:
starter_tool_info_import = STARTER_TOOLKIT_HEADER_IMPORT
starter_tool_info_warning = STARTER_TOOL_INFO_CALL.format(toolkit_name=toolkit_name)
else:
starter_tool_info_import = ""
starter_tool_info_warning = ""
try:
pyproject_path = find_pyproject_toml(toolkit_dir)
tool_info_description = get_pyproject_description(pyproject_path)
except ValueError:
tool_info_description = f"Enable Agents to interact with the {toolkit_name} MCP Server"
header = toolkit_header_template.format( header = toolkit_header_template.format(
toolkit_title=toolkit_name, toolkit_title=toolkit_name,
tool_info_description=tool_info_description,
starter_tool_info_import=starter_tool_info_import,
starter_tool_info_warning=starter_tool_info_warning,
description=generate_toolkit_description( description=generate_toolkit_description(
toolkit_name, toolkit_name,
[(tool.name, tool.description) for tool in tools], [(tool.name, tool.description) for tool in tools],
@ -94,7 +133,9 @@ def build_toolkit_mdx(
table_of_contents = build_table_of_contents(tools) table_of_contents = build_table_of_contents(tools)
footer = build_footer(toolkit_name, pip_package_name, sample_tool.requirements.authorization) footer = build_footer(toolkit_name, pip_package_name, sample_tool.requirements.authorization)
referenced_enums, tools_specs = build_tools_specs(tools, docs_section, enums) referenced_enums, tools_specs = build_tools_specs(
toolkit_package_name, tools, docs_section, enums
)
reference_mdx = build_reference_mdx(toolkit_name, referenced_enums) if referenced_enums else "" reference_mdx = build_reference_mdx(toolkit_name, referenced_enums) if referenced_enums else ""
toolkit_mdx = toolkit_page_template.format( toolkit_mdx = toolkit_page_template.format(
@ -201,6 +242,7 @@ def build_footer(
def build_tools_specs( def build_tools_specs(
toolkit_name: str,
tools: list[ToolDefinition], tools: list[ToolDefinition],
docs_section: str, docs_section: str,
enums: dict[str, type[Enum]], enums: dict[str, type[Enum]],
@ -212,6 +254,7 @@ def build_tools_specs(
referenced_enums = [] referenced_enums = []
for tool in tools: for tool in tools:
tool_referenced_enums, tool_spec = build_tool_spec( tool_referenced_enums, tool_spec = build_tool_spec(
toolkit_name=toolkit_name,
tool=tool, tool=tool,
docs_section=docs_section, docs_section=docs_section,
enums=enums, enums=enums,
@ -226,6 +269,7 @@ def build_tools_specs(
def build_tool_spec( def build_tool_spec(
toolkit_name: str,
tool: ToolDefinition, tool: ToolDefinition,
docs_section: str, docs_section: str,
enums: dict[str, type[Enum]], enums: dict[str, type[Enum]],
@ -234,7 +278,7 @@ def build_tool_spec(
tool_spec_secrets_template: str = TOOL_SPEC_SECRETS, tool_spec_secrets_template: str = TOOL_SPEC_SECRETS,
) -> tuple[list[tuple[str, type[Enum]]], str]: ) -> tuple[list[tuple[str, type[Enum]]], str]:
tabbed_examples_list = TABBED_EXAMPLES_LIST.format( tabbed_examples_list = TABBED_EXAMPLES_LIST.format(
toolkit_name=tool.toolkit.name.lower(), toolkit_name=toolkit_name.lower(),
tool_name=pascal_to_snake_case(tool.name), tool_name=pascal_to_snake_case(tool.name),
) )
referenced_enums, parameters = build_tool_parameters( referenced_enums, parameters = build_tool_parameters(
@ -290,7 +334,7 @@ def build_tool_parameters(
if schema.enum: if schema.enum:
enum_name, enum_class = find_enum_by_options(enums, schema.enum) enum_name, enum_class = find_enum_by_options(enums, schema.enum)
referenced_enums.append((enum_name, enum_class)) referenced_enums.append((enum_name, enum_class))
param_definition = f"`Enum` [{enum_name}](/toolkits/{docs_section}/{toolkit_name}/reference#{enum_name})" param_definition = f"`Enum` [{enum_name}](/mcp-servers/{docs_section}/{toolkit_name}/reference#{enum_name})"
else: else:
if schema.inner_val_type: if schema.inner_val_type:
param_definition = f"`{schema.val_type}[{schema.inner_val_type}]`" param_definition = f"`{schema.val_type}[{schema.inner_val_type}]`"
@ -325,12 +369,15 @@ def build_examples(
interface_signature = build_tool_interface_signature(tool) interface_signature = build_tool_interface_signature(tool)
input_map = generate_tool_input_map(interface_signature, openai_model) input_map = generate_tool_input_map(interface_signature, openai_model)
fully_qualified_name = tool.fully_qualified_name.split("@")[0] fully_qualified_name = tool.fully_qualified_name.split("@")[0]
py_file_name = f"{pascal_to_snake_case(tool.name)}_example_call_tool.py"
examples.append(( examples.append((
f"{pascal_to_snake_case(tool.name)}_example_call_tool.py", py_file_name,
build_python_example(fully_qualified_name, input_map), build_python_example(fully_qualified_name, input_map),
)) ))
js_file_name = f"{pascal_to_snake_case(tool.name)}_example_call_tool.js"
examples.append(( examples.append((
f"{pascal_to_snake_case(tool.name)}_example_call_tool.js", js_file_name,
build_javascript_example(fully_qualified_name, input_map), build_javascript_example(fully_qualified_name, input_map),
)) ))
return examples return examples
@ -376,18 +423,18 @@ def generate_toolkit_description(
"role": "system", "role": "system",
"content": ( "content": (
"You are a helpful assistant. " "You are a helpful assistant. "
"When given a toolkit name and a list of tools, you will generate a " "When given an MCP Server name and a list of tools, you will generate a "
"short, yet descriptive of the toolkit and the main actions a user " "short, yet descriptive of the MCP Server and the main actions a user "
"or LLM can perform with it.\n\n" "or LLM can perform with it.\n\n"
"As an example, here is the Asana toolkit description:\n\n" "As an example, here is the Asana MCP Server description:\n\n"
"The Arcade Asana toolkit provides a pre-built set of tools for " "The Arcade Asana MCP Server provides a pre-built set of tools for "
"interacting with Asana. These tools make it easy to build agents " "interacting with Asana. These tools make it easy to build agents "
"and AI apps that can:\n\n" "and AI apps that can:\n\n"
"- Manage teams, projects, and workspaces.\n" "- Manage teams, projects, and workspaces.\n"
"- Create, update, and search for tasks.\n" "- Create, update, and search for tasks.\n"
"- Retrieve data about tasks, projects, workspaces, users, etc.\n" "- Retrieve data about tasks, projects, workspaces, users, etc.\n"
"- Manage task attachments.\n\n" "- Manage task attachments.\n\n"
"And here is a JSON string with the list of tools in the Asana toolkit:\n\n" "And here is a JSON string with the list of tools in the Asana MCP Server:\n\n"
"```json\n\n" "```json\n\n"
'[["AttachFileToTask", "Attaches a file to an Asana task\n\nProvide exactly ' '[["AttachFileToTask", "Attaches a file to an Asana task\n\nProvide exactly '
"one of file_content_str, file_content_base64, or file_content_url, never " "one of file_content_str, file_content_base64, or file_content_url, never "
@ -412,18 +459,18 @@ def generate_toolkit_description(
'authenticated user"], ["MarkTaskAsCompleted", "Mark a task in Asana as ' 'authenticated user"], ["MarkTaskAsCompleted", "Mark a task in Asana as '
'completed"], ["UpdateTask", "Updates a task in Asana"]]\n\n```\n\n' 'completed"], ["UpdateTask", "Updates a task in Asana"]]\n\n```\n\n'
"Keep the description concise and to the point. The user will provide you with " "Keep the description concise and to the point. The user will provide you with "
"the toolkit name and the list of tools. Generate the description according to " "the MCP Server name and the list of tools. Generate the description according to "
"the instructions above." "the instructions above."
), ),
}, },
{ {
"role": "user", "role": "user",
"content": ( "content": (
f"The toolkit name is {toolkit_name} and the list of tools is:\n\n" f"The MCP Server name is {toolkit_name} and the list of tools is:\n\n"
"```json\n\n" "```json\n\n"
f"{json.dumps(tools, ensure_ascii=False)}\n\n" f"{json.dumps(tools, ensure_ascii=False)}\n\n"
"```\n\n" "```\n\n"
"Please generate a description for the toolkit." "Please generate a description for the MCP Server."
), ),
}, },
] ]

View file

@ -7,16 +7,21 @@ TOOLKIT_PAGE = """{header}
{footer} {footer}
""" """
TOOLKIT_HEADER = """# {toolkit_title} STARTER_TOOLKIT_HEADER_IMPORT = 'import StarterToolInfo from "@/app/_components/starter-tool-info";'
import ToolInfo from "@/components/ToolInfo"; STARTER_TOOL_INFO_CALL = '<StarterToolInfo toolkitName="{toolkit_name}" />'
import Badges from "@/components/Badges";
import TabbedCodeBlock from "@/components/TabbedCodeBlock"; TOOLKIT_HEADER = """# {toolkit_title}
import TableOfContents from "@/components/TableOfContents"; {starter_tool_info_import}
import ToolFooter from "@/components/ToolFooter"; import ToolInfo from "@/app/_components/tool-info";
import Badges from "@/app/_components/badges";
import TabbedCodeBlock from "@/app/_components/tabbed-code-block";
import TableOfContents from "@/app/_components/table-of-contents";
import ToolFooter from "@/app/_components/tool-footer";
import {{ Callout }} from "nextra/components";
<ToolInfo <ToolInfo
description="Enable agents to interact with {toolkit_title}" description="{tool_info_description}"
author="Arcade" author="Arcade"
{auth_type} {auth_type}
versions={{["{version}"]}} versions={{["{version}"]}}
@ -24,6 +29,8 @@ import ToolFooter from "@/components/ToolFooter";
<Badges repo="arcadeai/{pip_package_name}" /> <Badges repo="arcadeai/{pip_package_name}" />
{starter_tool_info_warning}
{description}""" {description}"""
TABLE_OF_CONTENTS = """## Available Tools TABLE_OF_CONTENTS = """## Available Tools
@ -36,11 +43,11 @@ TABLE_OF_CONTENTS = """## Available Tools
}} }}
/> />
<Tip> <Callout>
If you need to perform an action that's not listed here, you can [get in touch If you need to perform an action that's not listed here, you can [get in touch
with us](mailto:contact@arcade.dev) to request a new tool, or [create your with us](mailto:contact@arcade.dev) to request a new tool, or [create your
own tools](/home/build-tools/create-a-toolkit). own tools](/home/build-tools/create-a-mcp-server).
</Tip>""" </Callout>"""
TABLE_OF_CONTENTS_ITEM = '\n ["{tool_fully_qualified_name}", "{description}"],' TABLE_OF_CONTENTS_ITEM = '\n ["{tool_fully_qualified_name}", "{description}"],'
@ -59,7 +66,7 @@ TOOL_SPEC = """## {tool_fully_qualified_name}
TOOL_SPEC_SECRETS = """**Secrets** TOOL_SPEC_SECRETS = """**Secrets**
This tool requires the following secrets: {secrets} (learn how to [configure secrets](/home/build-tools/create-a-tool-with-secrets#supplying-the-secret)) This tool requires the following secrets: {secrets} (learn how to [configure secrets](/home/build-tools/create-a-tool-with-secrets#set-the-secret-in-the-arcade-dashboard))
""" """
TABBED_EXAMPLES_LIST = """<TabbedCodeBlock TABBED_EXAMPLES_LIST = """<TabbedCodeBlock
@ -67,8 +74,8 @@ TABBED_EXAMPLES_LIST = """<TabbedCodeBlock
{{ {{
label: "Call the Tool Directly", label: "Call the Tool Directly",
content: {{ content: {{
Python: ["/examples/integrations/toolkits/{toolkit_name}/{tool_name}_example_call_tool.py"], Python: ["/examples/integrations/mcp-servers/{toolkit_name}/{tool_name}_example_call_tool.py"],
JavaScript: ["/examples/integrations/toolkits/{toolkit_name}/{tool_name}_example_call_tool.js"], JavaScript: ["/examples/integrations/mcp-servers/{toolkit_name}/{tool_name}_example_call_tool.js"],
}}, }},
}}, }},
]}} ]}}
@ -85,9 +92,9 @@ TOOLKIT_FOOTER_OAUTH2 = """## Auth
<ToolFooter pipPackageName="{pip_package_name}" /> <ToolFooter pipPackageName="{pip_package_name}" />
""" """
WELL_KNOWN_PROVIDER_CONFIG = "The Arcade {toolkit_name} toolkit uses the [{provider_name} auth provider](/home/auth-providers/{provider_id}) to connect to users' {toolkit_name} accounts. Please refer to the [{provider_name} auth provider](/home/auth-providers/{provider_id}) documentation to learn how to configure auth." WELL_KNOWN_PROVIDER_CONFIG = "The Arcade {toolkit_name} MCP Server uses the [{provider_name} auth provider](/home/auth-providers/{provider_id}) to connect to users' {toolkit_name} accounts. Please refer to the [{provider_name} auth provider](/home/auth-providers/{provider_id}) documentation to learn how to configure auth."
GENERIC_PROVIDER_CONFIG = "The {toolkit_name} toolkit uses the Auth Provider with id `{provider_id}` to connect to users' {toolkit_name} accounts. In order to use the toolkit, you will need to configure the `{provider_id}` auth provider." GENERIC_PROVIDER_CONFIG = "The {toolkit_name} MCP Server uses the Auth Provider with id `{provider_id}` to connect to users' {toolkit_name} accounts. In order to use the MCP Server, you will need to configure the `{provider_id}` auth provider."
TOOL_CALL_EXAMPLE_JS = """import {{ Arcade }} from "@arcadeai/arcadejs"; TOOL_CALL_EXAMPLE_JS = """import {{ Arcade }} from "@arcadeai/arcadejs";
@ -148,7 +155,7 @@ print(json.dumps(response.output.value, indent=2))
ENUM_MDX = """## Reference ENUM_MDX = """## Reference
Below is a reference of enumerations used by some of the tools in the {toolkit_name} toolkit: Below is a reference of enumerations used by some of the tools in the {toolkit_name} MCP Server:
{enum_items} {enum_items}
""" """

View file

@ -2,13 +2,19 @@ import importlib
import inspect import inspect
import os import os
import re import re
import sys
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
if sys.version_info >= (3, 11):
import tomllib
else:
tomllib = None
from arcade_core.auth import AuthProviderType from arcade_core.auth import AuthProviderType
from arcade_core.catalog import ToolCatalog from arcade_core.catalog import ToolCatalog
from arcade_core.schema import ToolAuthRequirement, ToolDefinition from arcade_core.schema import ToolDefinition, ToolRequirements
from rich.console import Console from rich.console import Console
from arcade_cli.utils import discover_toolkits from arcade_cli.utils import discover_toolkits
@ -42,14 +48,22 @@ def write_file(path: str, content: str) -> None:
def read_toolkit_metadata(toolkit_dir: str) -> str: def read_toolkit_metadata(toolkit_dir: str) -> str:
pyproject_path = os.path.join(toolkit_dir, "pyproject.toml") pyproject_path = os.path.join(toolkit_dir, "pyproject.toml")
with open(pyproject_path) as f:
content = f.read() if tomllib is not None:
project_section_match = re.search(r"\[project\](.*?)(?=\n\[|$)", content, re.DOTALL) with open(pyproject_path, "rb") as f:
if project_section_match: data = tomllib.load(f)
project_content = project_section_match.group(1) if "project" in data and "name" in data["project"]:
name_match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', project_content) return data["project"]["name"]
if name_match: else:
return name_match.group(1).strip() # Fallback to regex for Python < 3.11
with open(pyproject_path) as f:
content = f.read()
project_section_match = re.search(r"\[project\](.*?)(?=\n\[|$)", content, re.DOTALL)
if project_section_match:
project_content = project_section_match.group(1)
name_match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', project_content)
if name_match:
return name_match.group(1).strip()
raise ValueError(f"Could not find package name in '{pyproject_path}'") raise ValueError(f"Could not find package name in '{pyproject_path}'")
@ -113,14 +127,15 @@ def get_all_enumerations(toolkit_root_dir: str) -> dict[str, type[Enum]]:
return enums return enums
def get_toolkit_auth_type(requirement: ToolAuthRequirement | None) -> str: def get_toolkit_auth_type(tool_req: ToolRequirements | None) -> str:
if requirement is None: if tool_req.authorization:
return "" if tool_req.authorization.provider_type == AuthProviderType.oauth2.value:
elif requirement.provider_type == AuthProviderType.oauth2.value: return 'authType="OAuth2"'
return 'authType="OAuth2"' else:
elif requirement.provider_type: return f'authType="{tool_req.authorization.provider_type}"'
return f'authType="{requirement.provider_type}"' elif tool_req.secrets:
return "" return 'authType="API Key"'
return 'authType="None"'
def find_enum_by_options( def find_enum_by_options(
@ -159,3 +174,47 @@ def is_well_known_provider(
def clean_fully_qualified_name(fully_qualified_name: str) -> str: def clean_fully_qualified_name(fully_qualified_name: str) -> str:
return fully_qualified_name.split("@")[0] return fully_qualified_name.split("@")[0]
def has_wrapper_tools_directory(toolkit_package_path: str) -> bool:
has_dir = os.path.exists(os.path.join(toolkit_package_path, "wrapper_tools"))
if has_dir:
return True
# Check one level deep
for dir_name in os.listdir(toolkit_package_path):
if os.path.exists(os.path.join(toolkit_package_path, dir_name, "wrapper_tools")):
return True
return False
def find_pyproject_toml(toolkit_package_path: str) -> str:
for root, _, files in os.walk(toolkit_package_path):
for file in files:
if file == "pyproject.toml":
return os.path.join(root, file)
raise ValueError(f"No pyproject.toml found in {toolkit_package_path}")
def get_pyproject_description(pyproject_path: str) -> str:
if tomllib is not None:
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
if "project" in data and "description" in data["project"]:
return data["project"]["description"]
else:
# Fallback to regex for Python < 3.11
with open(pyproject_path) as f:
content = f.read()
project_section_match = re.search(r"\[project\](.*?)(?=\n\[|$)", content, re.DOTALL)
if project_section_match:
project_content = project_section_match.group(1)
description_match = re.search(
r'description\s*=\s*["\']([^"\']+)["\']', project_content
)
if description_match:
return description_match.group(1).strip()
raise ValueError(f"Could not find description in '{pyproject_path}'")

View file

@ -14,7 +14,10 @@ from arcade_core.schema import ToolAuthRequirement
@patch("arcade_cli.toolkit_docs.utils.open") @patch("arcade_cli.toolkit_docs.utils.open")
def test_read_toolkit_metadata(mock_open): @patch("arcade_cli.toolkit_docs.utils.tomllib")
def test_read_toolkit_metadata(mock_tomllib, mock_open):
from unittest.mock import MagicMock, mock_open as mock_open_func
sample_pyproject_toml = """ sample_pyproject_toml = """
[build-system] [build-system]
requires = [ "hatchling",] requires = [ "hatchling",]
@ -74,13 +77,25 @@ skip_empty = true
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = [ "arcade_jira",] packages = [ "arcade_jira",]
""" """
mock_open.return_value.__enter__.return_value.read.return_value = sample_pyproject_toml
# Setup mock to handle both binary and text mode
def open_side_effect(path, mode="r"):
if mode == "rb":
return mock_open_func(read_data=sample_pyproject_toml.encode()).return_value
else:
return mock_open_func(read_data=sample_pyproject_toml).return_value
mock_open.side_effect = open_side_effect
mock_tomllib.load.return_value = {"project": {"name": "arcade_jira"}}
assert read_toolkit_metadata("path/to/toolkits/jira") == "arcade_jira" assert read_toolkit_metadata("path/to/toolkits/jira") == "arcade_jira"
mock_open.assert_called_once_with("path/to/toolkits/jira/pyproject.toml")
@patch("arcade_cli.toolkit_docs.utils.open") @patch("arcade_cli.toolkit_docs.utils.open")
def test_read_toolkit_metadata_missing_project_name(mock_open): @patch("arcade_cli.toolkit_docs.utils.tomllib")
def test_read_toolkit_metadata_missing_project_name(mock_tomllib, mock_open):
from unittest.mock import mock_open as mock_open_func
sample_pyproject_toml = """ sample_pyproject_toml = """
[build-system] [build-system]
requires = [ "hatchling",] requires = [ "hatchling",]
@ -98,9 +113,19 @@ dependencies = [
name = "Arcade" name = "Arcade"
email = "dev@arcade.dev" email = "dev@arcade.dev"
""" """
mock_open.return_value.__enter__.return_value.read.return_value = sample_pyproject_toml
# Setup mock to handle both binary and text mode
def open_side_effect(path, mode="r"):
if mode == "rb":
return mock_open_func(read_data=sample_pyproject_toml.encode()).return_value
else:
return mock_open_func(read_data=sample_pyproject_toml).return_value
mock_open.side_effect = open_side_effect
mock_tomllib.load.return_value = {"project": {}} # Missing "name"
with pytest.raises(ValueError): with pytest.raises(ValueError):
print("\n\n\n", read_toolkit_metadata("path/to/toolkits/jira")) read_toolkit_metadata("path/to/toolkits/jira")
def test_pascal_to_snake_case(): def test_pascal_to_snake_case():
@ -109,18 +134,23 @@ def test_pascal_to_snake_case():
def test_get_toolkit_auth_type_none(): def test_get_toolkit_auth_type_none():
assert get_toolkit_auth_type(requirement=None) == "" from arcade_core.schema import ToolRequirements
tool_req = ToolRequirements()
assert get_toolkit_auth_type(tool_req=tool_req) == 'authType="None"'
def test_get_toolkit_auth_type_with_provider_type(): def test_get_toolkit_auth_type_with_provider_type():
requirement = ToolAuthRequirement(provider_type=AuthProviderType.oauth2.value) from arcade_core.schema import ToolRequirements, ToolSecretRequirement
assert get_toolkit_auth_type(requirement=requirement) == 'authType="OAuth2"'
requirement = ToolAuthRequirement(provider_type="another_type") tool_req = ToolRequirements(authorization=ToolAuthRequirement(provider_type=AuthProviderType.oauth2.value))
assert get_toolkit_auth_type(requirement=requirement) == 'authType="another_type"' assert get_toolkit_auth_type(tool_req=tool_req) == 'authType="OAuth2"'
requirement = ToolAuthRequirement(provider_type="") tool_req = ToolRequirements(authorization=ToolAuthRequirement(provider_type="another_type"))
assert get_toolkit_auth_type(requirement=requirement) == "" assert get_toolkit_auth_type(tool_req=tool_req) == 'authType="another_type"'
tool_req = ToolRequirements(secrets=[ToolSecretRequirement(key="API_KEY")])
assert get_toolkit_auth_type(tool_req=tool_req) == 'authType="API Key"'
def test_is_well_known_provider_none(): def test_is_well_known_provider_none():

View file

@ -1,6 +1,6 @@
[project] [project]
name = "arcade-mcp" name = "arcade-mcp"
version = "1.0.0rc2" version = "1.0.0rc3"
description = "Arcade.dev - Tool Calling platform for Agents" description = "Arcade.dev - Tool Calling platform for Agents"
readme = "README.md" readme = "README.md"
license = {file = "LICENSE"} license = {file = "LICENSE"}