diff --git a/libs/arcade-cli/arcade_cli/main.py b/libs/arcade-cli/arcade_cli/main.py
index 2e6613e4..e849dde0 100644
--- a/libs/arcade-cli/arcade_cli/main.py
+++ b/libs/arcade-cli/arcade_cli/main.py
@@ -27,7 +27,6 @@ from arcade_cli.display import (
display_eval_results,
)
from arcade_cli.show import show_logic
-from arcade_cli.toolkit_docs import generate_toolkit_docs
from arcade_cli.usage.command_tracker import TrackedTyper, TrackedTyperGroup
from arcade_cli.utils import (
Provider,
@@ -707,115 +706,6 @@ def dashboard(
handle_cli_error("Failed to open dashboard", e, debug)
-@cli.command(
- help=(
- "Generate documentation for a server. "
- "Note: make sure to have the server installed in your current Python environment "
- "before running this command."
- ),
- rich_help_panel="Document",
- hidden=True,
-)
-def docs(
- server_name: str = typer.Option(
- ...,
- "--server-name",
- "-n",
- help="The name of the server to generate documentation for.",
- ),
- server_dir: str = typer.Option(
- ...,
- "--server-dir",
- "-t",
- help=(
- "The path to the server root directory (where the server code is implemented). "
- "Works with relative and absolute paths."
- ),
- ),
- docs_dir: str = typer.Option(
- ...,
- "--docs-dir",
- "-r",
- help="The path to the root of the Arcade docs repository. Works with relative and absolute paths.",
- ),
- docs_section: str = typer.Option(
- "",
- "--docs-section",
- "-s",
- help=(
- "The section of the docs to generate documentation for. E.g. 'productivity', 'sales'. "
- "This should be the name of the folder in /pages/tools. "
- "Defaults to an empty string (generate the docs in the root of /pages/tools)"
- ),
- ),
- openai_model: str = typer.Option(
- "gpt-5-mini",
- "--openai-model",
- "-m",
- help=(
- "A few parts of the documentation are generated using OpenAI API. "
- "Choose one of the 'gpt-4o' and 'gpt-5' series models."
- ),
- show_default=True,
- ),
- openai_api_key: str = typer.Option(
- None,
- "--openai-api-key",
- "-o",
- help="The OpenAI API key. If not provided, will get it from the `OPENAI_API_KEY` env var.",
- ),
- skip_tool_call_examples: bool = typer.Option(
- False,
- "--skip-tool-call-examples",
- "-se",
- help="Whether to skip generating tool call examples in Python and Javascript.",
- show_default=True,
- ),
- debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
-) -> None:
- if not openai_model.startswith("gpt-4o") and not openai_model.startswith("gpt-5"):
- console.print(
- f"Attention: '{openai_model}' is not a valid OpenAI model. "
- "Please choose one of the 'gpt-4o' and 'gpt-5' series models.",
- style="bold red",
- )
- handle_cli_error(
- f"Attention: '{openai_model}' is not a valid OpenAI model. "
- "Please choose one of the 'gpt-4o' and 'gpt-5' series models."
- )
-
- try:
- success = generate_toolkit_docs(
- console=console,
- toolkit_name=server_name,
- toolkit_dir=server_dir,
- docs_dir=docs_dir,
- docs_section=docs_section,
- openai_model=openai_model,
- openai_api_key=openai_api_key,
- tool_call_examples=not skip_tool_call_examples,
- debug=debug,
- )
- except Exception as error:
- handle_cli_error(
- message=f"Failed to generate documentation for '{server_name}' in '{docs_dir}'",
- error=error,
- debug=debug,
- )
- success = False
-
- if success:
- console.print(
- f"Generated documentation for '{server_name}' in '{docs_dir}'",
- style="bold green",
- )
- else:
- console.print(
- f"Failed to generate documentation for '{server_name}' in '{docs_dir}'",
- style="bold red",
- )
-
-
@cli.callback()
def main_callback(
ctx: typer.Context,
diff --git a/libs/arcade-cli/arcade_cli/toolkit_docs/__init__.py b/libs/arcade-cli/arcade_cli/toolkit_docs/__init__.py
deleted file mode 100644
index b13ab96b..00000000
--- a/libs/arcade-cli/arcade_cli/toolkit_docs/__init__.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from functools import partial
-
-import openai
-from rich.console import Console
-
-from arcade_cli.toolkit_docs.docs_builder import (
- build_example_path,
- build_examples,
- build_toolkit_mdx,
- build_toolkit_mdx_file_path,
-)
-from arcade_cli.toolkit_docs.utils import (
- get_all_enumerations,
- get_list_of_tools,
- has_wrapper_tools_directory,
- print_debug_func,
- read_toolkit_metadata,
- resolve_api_key,
- standardize_dir_path,
- write_file,
-)
-
-
-def generate_toolkit_docs(
- console: Console,
- toolkit_name: str,
- toolkit_dir: str,
- docs_section: str,
- docs_dir: str,
- openai_model: str,
- openai_api_key: str | None = None,
- tool_call_examples: bool = True,
- debug: bool = False,
-) -> bool:
- openai.api_key = resolve_api_key(openai_api_key, "OPENAI_API_KEY")
-
- if not openai.api_key:
- console.print(
- "❌ Provide --openai-api-key argument or set the OPENAI_API_KEY environment variable",
- style="red",
- )
- return False
-
- print_debug = partial(print_debug_func, debug, console)
-
- docs_dir = standardize_dir_path(docs_dir)
- toolkit_dir = standardize_dir_path(toolkit_dir)
- is_wrapper_toolkit = has_wrapper_tools_directory(toolkit_dir)
-
- print_debug("Reading server metadata")
- pip_package_name = read_toolkit_metadata(toolkit_dir)
-
- print_debug(f"Getting list of tools for {toolkit_name} from the local Python environment")
- tools = get_list_of_tools(toolkit_name)
-
- print_debug(f"Found {len(tools)} tools")
-
- print_debug("Getting all enumerations potentially used in tool argument specs")
- enums = get_all_enumerations(toolkit_dir)
-
- 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_package_name=toolkit_name,
- toolkit_dir=toolkit_dir,
- tools=tools,
- docs_section=docs_section,
- enums=enums,
- pip_package_name=pip_package_name,
- openai_model=openai_model,
- is_wrapper_toolkit=is_wrapper_toolkit,
- )
- write_file(toolkit_mdx_file_path, toolkit_mdx)
-
- if tool_call_examples:
- print_debug("Building tool-call examples in Python and JavaScript")
- examples = build_examples(print_debug, tools, openai_model)
-
- for filename, example in examples:
- example_path = build_example_path(filename, docs_dir, toolkit_name)
- write_file(example_path, example)
-
- print_debug(f"Done generating docs for {toolkit_name}")
-
- return True
diff --git a/libs/arcade-cli/arcade_cli/toolkit_docs/docs_builder.py b/libs/arcade-cli/arcade_cli/toolkit_docs/docs_builder.py
deleted file mode 100644
index e337c199..00000000
--- a/libs/arcade-cli/arcade_cli/toolkit_docs/docs_builder.py
+++ /dev/null
@@ -1,603 +0,0 @@
-import json
-import os
-import pprint
-from enum import Enum
-from typing import Any, Callable, cast
-
-import openai
-from arcade_core import auth as auth_module
-from arcade_core.schema import (
- ToolAuthRequirement,
- ToolDefinition,
- ToolInput,
- ToolSecretRequirement,
-)
-from rich.console import Console
-
-from arcade_cli.toolkit_docs.templates import (
- ENUM_ITEM,
- ENUM_MDX,
- ENUM_VALUE,
- GENERIC_PROVIDER_CONFIG,
- STARTER_TOOL_INFO_CALL,
- STARTER_TOOLKIT_HEADER_IMPORT,
- TABBED_EXAMPLES_LIST,
- TABLE_OF_CONTENTS,
- TABLE_OF_CONTENTS_ITEM,
- TOOL_CALL_EXAMPLE_JS,
- TOOL_CALL_EXAMPLE_PY,
- TOOL_PARAMETER,
- TOOL_SPEC,
- TOOL_SPEC_SECRETS,
- TOOLKIT_FOOTER,
- TOOLKIT_FOOTER_OAUTH2,
- TOOLKIT_HEADER,
- TOOLKIT_PAGE,
- WELL_KNOWN_PROVIDER_CONFIG,
-)
-from arcade_cli.toolkit_docs.utils import (
- clean_fully_qualified_name,
- find_enum_by_options,
- find_pyproject_toml,
- get_pyproject_description,
- get_toolkit_auth_type,
- is_well_known_provider,
- pascal_to_snake_case,
-)
-
-console = Console()
-
-
-def build_toolkit_mdx_dir_path(
- docs_section: str,
- docs_root_dir: str,
- toolkit_name: str,
- ensure_exists: bool = True,
-) -> str:
- dir_path = os.path.join(
- docs_root_dir,
- "app",
- "en",
- "mcp-servers",
- docs_section,
- 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:
- return os.path.join(
- docs_root_dir,
- "public",
- "examples",
- "integrations",
- "mcp-servers",
- toolkit_name.lower(),
- example_filename,
- )
-
-
-def build_toolkit_mdx(
- toolkit_package_name: str,
- toolkit_dir: str,
- tools: list[ToolDefinition],
- docs_section: str,
- enums: dict[str, type[Enum]],
- pip_package_name: str,
- openai_model: str,
- toolkit_header_template: str = TOOLKIT_HEADER,
- toolkit_page_template: str = TOOLKIT_PAGE,
- is_wrapper_toolkit: bool = False,
-) -> tuple[str, str]:
- sample_tool = tools[0]
- toolkit_name = sample_tool.toolkit.name
- toolkit_version = sample_tool.toolkit.version
- 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(
- 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(
- toolkit_name,
- [(tool.name, tool.description) for tool in tools],
- openai_model,
- ),
- pip_package_name=pip_package_name,
- auth_type=auth_type,
- version=toolkit_version,
- )
- table_of_contents = build_table_of_contents(tools)
- footer = build_footer(toolkit_name, pip_package_name, sample_tool.requirements.authorization)
-
- 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 ""
-
- toolkit_mdx = toolkit_page_template.format(
- header=header,
- table_of_contents=table_of_contents,
- tools_specs=tools_specs,
- reference_mdx=reference_mdx,
- footer=footer,
- )
-
- return toolkit_mdx.strip()
-
-
-def build_reference_mdx(
- toolkit_name: str,
- referenced_enums: list[tuple[str, type[Enum]]],
- enum_item_template: str = ENUM_ITEM,
- enum_value_template: str = ENUM_VALUE,
- enum_mdx_template: str = ENUM_MDX,
-) -> str:
- enum_items = ""
- enum_names_seen = set()
-
- for enum_name, enum_class in referenced_enums:
- if enum_name in enum_names_seen:
- continue
- enum_names_seen.add(enum_name)
- enum_items += enum_item_template.format(
- enum_name=enum_name,
- enum_values=build_enum_values(
- enum_class=enum_class,
- enum_value_template=enum_value_template,
- ),
- )
-
- return enum_mdx_template.format(
- toolkit_name=toolkit_name,
- enum_items=enum_items,
- )
-
-
-def build_enum_values(
- enum_class: type[Enum],
- enum_value_template: str = ENUM_VALUE,
-) -> str:
- enum_values = ""
- for enum_member in enum_class:
- enum_values += (
- enum_value_template.format(
- enum_option_name=enum_member.name,
- enum_option_value=enum_member.value,
- )
- + "\n"
- )
- return enum_values
-
-
-def build_table_of_contents(
- tools: list[ToolDefinition],
- table_of_contents_item_template: str = TABLE_OF_CONTENTS_ITEM,
- table_of_contents_template: str = TABLE_OF_CONTENTS,
-) -> str:
- tools_items = ""
-
- for tool in tools:
- tools_items += table_of_contents_item_template.format(
- tool_fully_qualified_name=clean_fully_qualified_name(tool.fully_qualified_name),
- description=tool.description.split("\n")[0],
- )
-
- return table_of_contents_template.format(tool_items=tools_items)
-
-
-def build_footer(
- toolkit_name: str,
- pip_package_name: str,
- authorization: ToolAuthRequirement | None,
- footer_template: str = TOOLKIT_FOOTER,
- oauth2_footer_template: str = TOOLKIT_FOOTER_OAUTH2,
- well_known_provider_config_template: str = WELL_KNOWN_PROVIDER_CONFIG,
- generic_provider_config_template: str = GENERIC_PROVIDER_CONFIG,
-) -> str:
- if authorization and authorization.provider_type == "oauth2" and authorization.provider_id:
- is_well_known = is_well_known_provider(
- provider_id=authorization.provider_id,
- auth_module=auth_module,
- )
- config_template = (
- well_known_provider_config_template
- if is_well_known
- else generic_provider_config_template
- )
- provider_configuration = config_template.format(
- toolkit_name=toolkit_name,
- provider_id=authorization.provider_id,
- provider_name=authorization.provider_id.capitalize(),
- )
-
- return oauth2_footer_template.format(
- pip_package_name=pip_package_name,
- provider_configuration=provider_configuration,
- )
- return footer_template.format(toolkit_name=toolkit_name, pip_package_name=pip_package_name)
-
-
-def build_tools_specs(
- toolkit_name: str,
- tools: list[ToolDefinition],
- docs_section: str,
- enums: dict[str, type[Enum]],
- tool_spec_template: str = TOOL_SPEC,
- tool_parameter_template: str = TOOL_PARAMETER,
- tool_spec_secrets_template: str = TOOL_SPEC_SECRETS,
-) -> tuple[list[tuple[str, type[Enum]]], str]:
- tools_specs = ""
- referenced_enums = []
- for tool in tools:
- tool_referenced_enums, tool_spec = build_tool_spec(
- toolkit_name=toolkit_name,
- tool=tool,
- docs_section=docs_section,
- enums=enums,
- tool_spec_template=tool_spec_template,
- tool_parameter_template=tool_parameter_template,
- tool_spec_secrets_template=tool_spec_secrets_template,
- )
- tools_specs += tool_spec
- referenced_enums.extend(tool_referenced_enums)
-
- return referenced_enums, tools_specs
-
-
-def build_tool_spec(
- toolkit_name: str,
- tool: ToolDefinition,
- docs_section: str,
- enums: dict[str, type[Enum]],
- tool_spec_template: str = TOOL_SPEC,
- tool_parameter_template: str = TOOL_PARAMETER,
- tool_spec_secrets_template: str = TOOL_SPEC_SECRETS,
-) -> tuple[list[tuple[str, type[Enum]]], str]:
- tabbed_examples_list = TABBED_EXAMPLES_LIST.format(
- toolkit_name=toolkit_name.lower(),
- tool_name=pascal_to_snake_case(tool.name),
- )
- referenced_enums, parameters = build_tool_parameters(
- tool_input=tool.input,
- docs_section=docs_section,
- toolkit_name=tool.toolkit.name.lower(),
- enums=enums,
- tool_parameter_template=tool_parameter_template,
- )
-
- if not parameters:
- parameters = "This tool does not take any parameters."
-
- secrets = (
- build_tool_secrets(
- secrets=tool.requirements.secrets,
- template=tool_spec_secrets_template,
- )
- if tool.requirements.secrets
- else ""
- )
-
- return referenced_enums, tool_spec_template.format(
- tool_fully_qualified_name=clean_fully_qualified_name(tool.fully_qualified_name),
- tabbed_examples_list=tabbed_examples_list,
- description=tool.description.split("\n")[0],
- parameters=parameters,
- secrets=secrets,
- )
-
-
-def build_tool_secrets(
- secrets: list[ToolSecretRequirement],
- template: str = TOOL_SPEC_SECRETS,
-) -> str:
- if not secrets:
- return ""
- secret_keys_str = "`, `".join([secret.key for secret in secrets])
- return template.format(secrets=f"`{secret_keys_str}`")
-
-
-def build_tool_parameters(
- tool_input: ToolInput,
- docs_section: str,
- toolkit_name: str,
- enums: dict[str, type[Enum]],
- tool_parameter_template: str = TOOL_PARAMETER,
-) -> tuple[list[tuple[str, type[Enum]]], str]:
- referenced_enums = []
- parameters = ""
- for parameter in tool_input.parameters:
- schema = parameter.value_schema
- if schema.enum:
- enum_name, enum_class = find_enum_by_options(enums, schema.enum)
- referenced_enums.append((enum_name, enum_class))
- param_definition = f"`Enum` [{enum_name}](/mcp-servers/{docs_section}/{toolkit_name}/reference#{enum_name})"
- else:
- if schema.inner_val_type:
- param_definition = f"`{schema.val_type}[{schema.inner_val_type}]`"
- else:
- param_definition = f"`{schema.val_type}`"
-
- if parameter.required:
- param_definition += ", required"
- else:
- param_definition += ", optional"
-
- parameters += (
- tool_parameter_template.format(
- param_name=parameter.name,
- definition=param_definition,
- description=parameter.description,
- )
- + "\n"
- )
-
- return referenced_enums, parameters
-
-
-def build_examples(
- print_debug: Callable,
- tools: list[ToolDefinition],
- openai_model: str,
-) -> list[tuple[str, str]]:
- examples = []
- for tool in tools:
- print_debug(f"Generating tool-call examples for {tool.name}")
- interface_signature = build_tool_interface_signature(tool)
- input_map = generate_tool_input_map(interface_signature, openai_model)
- 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((
- py_file_name,
- build_python_example(fully_qualified_name, input_map),
- ))
- js_file_name = f"{pascal_to_snake_case(tool.name)}_example_call_tool.js"
- examples.append((
- js_file_name,
- build_javascript_example(fully_qualified_name, input_map),
- ))
- return examples
-
-
-def build_python_example(
- tool_fully_qualified_name: str,
- input_map: dict[str, Any],
- template: str = TOOL_CALL_EXAMPLE_PY,
-) -> str:
- input_map_str = pprint.pformat(
- input_map,
- indent=4,
- width=100,
- compact=False,
- sort_dicts=False,
- )
- input_map_str = "{\n " + input_map_str.lstrip("{ ").rstrip("}") + "\n}" # noqa: B005
- return template.format(
- tool_fully_qualified_name=tool_fully_qualified_name,
- input_map=input_map_str,
- )
-
-
-def build_javascript_example(
- tool_fully_qualified_name: str,
- input_map: dict,
- template: str = TOOL_CALL_EXAMPLE_JS,
-) -> str:
- return template.format(
- tool_fully_qualified_name=tool_fully_qualified_name,
- input_map=json.dumps(input_map, indent=2, ensure_ascii=False),
- )
-
-
-def generate_toolkit_description(
- toolkit_name: str,
- tools: list[tuple[str, str]],
- openai_model: str,
-) -> str:
- messages = [
- {
- "role": "system",
- "content": (
- "You are a helpful assistant. "
- "When given an MCP Server name and a list of tools, you will generate a "
- "short, yet descriptive of the MCP Server and the main actions a user "
- "or LLM can perform with it.\n\n"
- "As an example, here is the Asana MCP Server description:\n\n"
- "The Arcade Asana MCP Server provides a pre-built set of tools for "
- "interacting with Asana. These tools make it easy to build agents "
- "and AI apps that can:\n\n"
- "- Manage teams, projects, and workspaces.\n"
- "- Create, update, and search for tasks.\n"
- "- Retrieve data about tasks, projects, workspaces, users, etc.\n"
- "- Manage task attachments.\n\n"
- "And here is a JSON string with the list of tools in the Asana MCP Server:\n\n"
- "```json\n\n"
- '[["AttachFileToTask", "Attaches a file to an Asana task\n\nProvide exactly '
- "one of file_content_str, file_content_base64, or file_content_url, never "
- "more\nthan one.\n\n- Use file_content_str for text files (will be encoded "
- "using file_encoding)\n- Use file_content_base64 for binary files like images, "
- 'PDFs, etc.\n- Use file_content_url if the file is hosted on an external URL"], '
- '["CreateTag", "Create a tag in Asana"], ["CreateTask", "Creates a task in '
- "Asana\n\nThe task must be associated to at least one of the following: "
- "parent_task_id, project, or\nworkspace_id. If none of these are provided and "
- "the account has only one workspace, the task\nwill be associated to that "
- "workspace. If the account has multiple workspaces, an error will\nbe raised "
- 'with a list of available workspaces."], ["GetProjectById", "Get an Asana '
- 'project by its ID"], ["GetSubtasksFromATask", "Get the subtasks of a task"], '
- '["GetTagById", "Get an Asana tag by its ID"], ["GetTaskById", "Get a task by '
- 'its ID"], ["GetTasksWithoutId", "Search for tasks"], ["GetTeamById", "Get an '
- 'Asana team by its ID"], ["GetUserById", "Get a user by ID"], ["GetWorkspaceById", '
- '"Get an Asana workspace by its ID"], ["ListProjects", "List projects in Asana"], '
- '["ListTags", "List tags in an Asana workspace"], ["ListTeams", "List teams in '
- 'an Asana workspace"], ["ListTeamsTheCurrentUserIsAMemberOf", "List teams in '
- 'Asana that the current user is a member of"], ["ListUsers", "List users in '
- 'Asana"], ["ListWorkspaces", "List workspaces in Asana that are visible to the '
- 'authenticated user"], ["MarkTaskAsCompleted", "Mark a task in Asana as '
- '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 "
- "the MCP Server name and the list of tools. Generate the description according to "
- "the instructions above."
- ),
- },
- {
- "role": "user",
- "content": (
- f"The MCP Server name is {toolkit_name} and the list of tools is:\n\n"
- "```json\n\n"
- f"{json.dumps(tools, ensure_ascii=False)}\n\n"
- "```\n\n"
- "Please generate a description for the MCP Server."
- ),
- },
- ]
-
- return request_openai_generation(model=openai_model, max_tokens=512, messages=messages)
-
-
-def generate_tool_input_map(
- interface_signature: dict[str, Any],
- openai_model: str,
- retries: int = 0,
- max_retries: int = 3,
-) -> dict[str, Any]:
- messages = [
- {
- "role": "system",
- "content": (
- "You are a helpful assistant expert in generating data for documenting "
- "sample scripts to calling tools. A tool is a function that is used in "
- "context of LLM tool-calling / function-calling.\n\n"
- "When given a tool signature with typed arguments, "
- "you must return exactly one JSON object (no markdown, no extra text) "
- "where each key is an argument name, and each value is a sample value "
- "for that argument that would make sense in a sample script to showcase "
- "human software engineers how the tool may be called. Generate the "
- "argument sample value based on its name and description\n\n"
- "Not every single argument must always be present in the input map. "
- "In some cases, the tool may require only one of two arguments to be "
- "provided, for example. In such cases, an indication will be present "
- "either/or in the tool description or the argument description. "
- "Always follow such instructions when present in the tool interface.\n\n"
- "Keep argument values as short as possible. Values don't have to always "
- "be valid. For instance, for file content base64-encoded arguments, "
- "you can use a short text or a placeholder like `[file_content]`, it is "
- "not necessary that the value is a valid base64-encoded string.\n\n"
- "Remember that you MUST RESPOND ONLY WITH A VALID JSON STRING, NO ADDED "
- "TEXT. Your response will be json.load'ed, so it must be a valid JSON "
- "string."
- ),
- },
- {
- "role": "user",
- "content": (
- "Here is a tool interface:\n\n"
- f"{json.dumps(interface_signature, ensure_ascii=False)}\n\n"
- "Please provide a sample input map as a JSON object."
- ),
- },
- ]
-
- text = request_openai_generation(model=openai_model, max_tokens=512, messages=messages)
-
- try:
- return cast(dict[str, Any], json.loads(text))
- except (json.JSONDecodeError, TypeError):
- if retries < max_retries:
- return generate_tool_input_map(
- interface_signature=interface_signature,
- openai_model=openai_model,
- retries=retries + 1,
- max_retries=max_retries,
- )
- tool_name = interface_signature["tool_name"]
- console.print(
- f"Attention: {openai_model} failed to generate a valid inputs JSON for the tool '{tool_name}'. "
- "Please check the Python & Javascript example scripts generated and enter a sample input manually.",
- style="red",
- )
- return {}
-
-
-def build_tool_interface_signature(tool: ToolDefinition) -> dict[str, Any]:
- args = []
- for arg in tool.input.parameters:
- data: dict[str, Any] = {
- "arg_name": arg.name,
- "arg_description": arg.description,
- "is_arg_required": arg.required,
- "arg_type": arg.value_schema.val_type,
- }
-
- if arg.value_schema.enum:
- data["enum"] = {
- "accepted_values": arg.value_schema.enum,
- }
-
- args.append(data)
-
- return {
- "tool_name": tool.name,
- "tool_description": tool.description,
- "tool_args": args,
- }
-
-
-def request_openai_generation(
- model: str,
- max_tokens: int,
- messages: list[dict[str, Any]],
-) -> str:
- if model.startswith("gpt-5"):
- response = openai.responses.create(
- model=model,
- input=messages,
- max_output_tokens=max_tokens,
- reasoning={
- "effort": "minimal",
- },
- text={
- "verbosity": "low",
- },
- )
- response_str = cast(str, response.output_text)
-
- elif model.startswith("gpt-4o"):
- response = openai.chat.completions.create(
- model=model,
- messages=messages,
- temperature=0.0,
- max_completion_tokens=max_tokens,
- stop=["\n\n"],
- )
- response_str = cast(str, response.choices[0].message.content)
-
- else:
- raise ValueError(
- f"Unsupported OpenAI model: {model}. Choose a model from the 'gpt-4o' or 'gpt-5' series."
- )
-
- return response_str.strip()
diff --git a/libs/arcade-cli/arcade_cli/toolkit_docs/templates.py b/libs/arcade-cli/arcade_cli/toolkit_docs/templates.py
deleted file mode 100644
index 399bc8d7..00000000
--- a/libs/arcade-cli/arcade_cli/toolkit_docs/templates.py
+++ /dev/null
@@ -1,168 +0,0 @@
-TOOLKIT_PAGE = """{header}
-
-{table_of_contents}
-
-{tools_specs}
-{reference_mdx}
-{footer}
-"""
-
-STARTER_TOOLKIT_HEADER_IMPORT = 'import StarterToolInfo from "@/app/_components/starter-tool-info";'
-
-STARTER_TOOL_INFO_CALL = ''
-
-TOOLKIT_HEADER = """# {toolkit_title}
-{starter_tool_info_import}
-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";
-
-
-
-
-
-{starter_tool_info_warning}
-
-{description}"""
-
-TABLE_OF_CONTENTS = """## Available Tools
-
-
-
-
- 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
- own tools](/home/build-tools/create-a-mcp-server).
-"""
-
-TABLE_OF_CONTENTS_ITEM = '\n ["{tool_fully_qualified_name}", "{description}"],'
-
-TOOL_SPEC = """## {tool_fully_qualified_name}
-
-
-{tabbed_examples_list}
-
-{description}
-
-**Parameters**
-
-{parameters}
-{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#set-the-secret-in-the-arcade-dashboard))
-"""
-
-TABBED_EXAMPLES_LIST = """"""
-
-TOOL_PARAMETER = "- **{param_name}** ({definition}) {description}"
-
-TOOLKIT_FOOTER = """"""
-
-TOOLKIT_FOOTER_OAUTH2 = """## Auth
-
-{provider_configuration}
-
-
-"""
-
-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} 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";
-
-const client = new Arcade(); // Automatically finds the `ARCADE_API_KEY` env variable
-
-const USER_ID = "{{arcade_user_id}}";
-const TOOL_NAME = "{tool_fully_qualified_name}";
-
-// Start the authorization process
-const authResponse = await client.tools.authorize({{tool_name: TOOL_NAME}});
-
-if (authResponse.status !== "completed") {{
- console.log(`Click this link to authorize: ${{authResponse.url}}`);
-}}
-
-// Wait for the authorization to complete
-await client.auth.waitForCompletion(authResponse);
-
-const toolInput = {input_map};
-
-const response = await client.tools.execute({{
- tool_name: TOOL_NAME,
- input: toolInput,
- user_id: USER_ID,
-}});
-
-console.log(JSON.stringify(response.output.value, null, 2));
-"""
-
-TOOL_CALL_EXAMPLE_PY = """import json
-from arcadepy import Arcade
-
-client = Arcade() # Automatically finds the `ARCADE_API_KEY` env variable
-
-USER_ID = "{{arcade_user_id}}"
-TOOL_NAME = "{tool_fully_qualified_name}"
-
-auth_response = client.tools.authorize(
- tool_name=TOOL_NAME,
- user_id=USER_ID,
-)
-
-if auth_response.status != "completed":
- print(f"Click this link to authorize: {{auth_response.url}}")
-
-# Wait for the authorization to complete
-client.auth.wait_for_completion(auth_response)
-
-tool_input = {input_map}
-
-response = client.tools.execute(
- tool_name=TOOL_NAME,
- input=tool_input,
- user_id=USER_ID,
-)
-print(json.dumps(response.output.value, indent=2))
-"""
-
-ENUM_MDX = """## Reference
-
-Below is a reference of enumerations used by some of the tools in the {toolkit_name} MCP Server:
-
-{enum_items}
-"""
-
-ENUM_ITEM = """## {enum_name}
-
-{enum_values}
-"""
-
-ENUM_VALUE = "- **{enum_option_name}**: `{enum_option_value}`"
diff --git a/libs/arcade-cli/arcade_cli/toolkit_docs/utils.py b/libs/arcade-cli/arcade_cli/toolkit_docs/utils.py
deleted file mode 100644
index b5d105d2..00000000
--- a/libs/arcade-cli/arcade_cli/toolkit_docs/utils.py
+++ /dev/null
@@ -1,220 +0,0 @@
-import importlib
-import inspect
-import os
-import re
-import sys
-from enum import Enum
-from pathlib import Path
-from types import ModuleType
-
-if sys.version_info >= (3, 11):
- import tomllib
-else:
- tomllib = None
-
-from arcade_core.auth import AuthProviderType
-from arcade_core.catalog import ToolCatalog
-from arcade_core.schema import ToolDefinition, ToolRequirements
-from rich.console import Console
-
-from arcade_cli.utils import discover_toolkits
-
-
-def print_debug_func(debug: bool, console: Console, message: str, style: str = "dim") -> None:
- if not debug:
- return
- console.print(message, style=style)
-
-
-def standardize_dir_path(dir_path: str) -> str:
- dir_path = dir_path.rstrip("/") + "/"
- return os.path.expanduser(dir_path)
-
-
-def resolve_api_key(cli_input_value: str | None, env_var_name: str) -> str | None:
- if cli_input_value:
- return cli_input_value
- elif os.getenv(env_var_name):
- return os.getenv(env_var_name)
- else:
- return None
-
-
-def write_file(path: str, content: str) -> None:
- os.makedirs(os.path.dirname(path), exist_ok=True)
- with open(path, "w") as f:
- f.write(content)
-
-
-def read_toolkit_metadata(toolkit_dir: str) -> str:
- pyproject_path = os.path.join(toolkit_dir, "pyproject.toml")
-
- if tomllib is not None:
- with open(pyproject_path, "rb") as f:
- data = tomllib.load(f)
- if "project" in data and "name" in data["project"]:
- return data["project"]["name"]
- 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)
- 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}'")
-
-
-def pascal_to_snake_case(text: str) -> str:
- return re.sub(r"(? list[ToolDefinition]:
- tools = []
- toolkits = discover_toolkits()
-
- for toolkit in toolkits:
- if toolkit.name.casefold() == toolkit_name.casefold():
- for module_name, module_tools in toolkit.tools.items():
- module = importlib.import_module(module_name)
- for tool_name in module_tools:
- tool_func = getattr(module, tool_name)
- tool = ToolCatalog.create_tool_definition(
- tool_func, toolkit.name, toolkit.version, toolkit.description
- )
- tools.append(tool)
-
- if not tools:
- raise ValueError(
- f"Tools not found for the toolkit '{toolkit_name}'. Make sure to have the toolkit "
- "installed in your current Python environment."
- )
-
- return tools
-
-
-def get_all_enumerations(toolkit_root_dir: str) -> dict[str, type[Enum]]:
- enums = {}
- toolkit_path = Path(toolkit_root_dir)
-
- for py_file in toolkit_path.rglob("*.py"):
- if py_file.name == "__init__.py":
- continue
-
- if ".venv" in py_file.parts or "venv" in py_file.parts:
- continue
-
- module_name = py_file.stem
- spec = importlib.util.spec_from_file_location(module_name, py_file)
- if spec is None or spec.loader is None:
- continue
-
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
-
- for name, obj in inspect.getmembers(module):
- if (
- name not in enums
- and inspect.isclass(obj)
- and issubclass(obj, Enum)
- and obj is not Enum
- ):
- enums[name] = obj
-
- return enums
-
-
-def get_toolkit_auth_type(tool_req: ToolRequirements | None) -> str:
- if tool_req.authorization:
- if tool_req.authorization.provider_type == AuthProviderType.oauth2.value:
- return 'authType="OAuth2"'
- else:
- return f'authType="{tool_req.authorization.provider_type}"'
- elif tool_req.secrets:
- return 'authType="API Key"'
- return 'authType="None"'
-
-
-def find_enum_by_options(
- enums: dict[str, type[Enum]], options: list[str]
-) -> tuple[str, type[Enum]]:
- options_set = set(options)
- for enum_name, enum_class in enums.items():
- enum_member_values = [member.value for member in enum_class]
- if set(enum_member_values) == options_set:
- return enum_name, enum_class
- raise ValueError(f"No enum found for options: {options_set}")
-
-
-def is_well_known_provider(
- provider_id: str | None,
- auth_module: ModuleType,
-) -> bool:
- if provider_id is None:
- return False
-
- for _, obj in inspect.getmembers(auth_module, inspect.isclass):
- if not issubclass(obj, auth_module.OAuth2) or obj is auth_module.OAuth2:
- continue
- try:
- instance = obj()
- except AttributeError:
- continue
- provider_id_matches = (
- hasattr(instance, "provider_id") and instance.provider_id == provider_id
- )
- if provider_id_matches:
- return True
-
- return False
-
-
-def clean_fully_qualified_name(fully_qualified_name: str) -> str:
- 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}'")
diff --git a/libs/tests/cli/toolkit_docs/test_docs_builder.py b/libs/tests/cli/toolkit_docs/test_docs_builder.py
deleted file mode 100644
index 1f78182b..00000000
--- a/libs/tests/cli/toolkit_docs/test_docs_builder.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import json
-
-from arcade_cli.toolkit_docs.docs_builder import build_javascript_example, build_python_example
-from arcade_cli.toolkit_docs.templates import TOOL_CALL_EXAMPLE_JS, TOOL_CALL_EXAMPLE_PY
-
-
-def test_build_javascript_example():
- tool_fully_qualified_name = "Toolkit.ToolName"
-
- input_map = {
- "str_arg": "str_value",
- "fake_bool_value": "true",
- "fake_bool_phrase": "this is not a true boolean",
- "int_arg": 123,
- "bool_arg": True,
- "list_arg": ["item1", "item2"],
- "dict_arg": {"key1": "value1", "key2": "value2"},
- "list_of_bool": [True, False],
- }
-
- response = build_javascript_example(tool_fully_qualified_name, input_map, TOOL_CALL_EXAMPLE_JS)
- assert response == TOOL_CALL_EXAMPLE_JS.format(
- tool_fully_qualified_name=tool_fully_qualified_name,
- input_map=json.dumps(input_map, indent=2, ensure_ascii=False),
- )
-
-
-def test_build_python_example():
- tool_fully_qualified_name = "Toolkit.ToolName"
-
- input_map = {
- "str_arg": "str_value",
- "fake_bool_value": "true",
- "fake_bool_phrase": "this is not a true boolean",
- "int_arg": 123,
- "bool_arg": True,
- "list_arg": ["item1", "item2"],
- "dict_arg": {"key1": "value1", "key2": "value2"},
- "list_of_bool": [True, False],
- }
-
- input_map_str = """{
- 'str_arg': 'str_value',
- 'fake_bool_value': 'true',
- 'fake_bool_phrase': 'this is not a true boolean',
- 'int_arg': 123,
- 'bool_arg': True,
- 'list_arg': ['item1', 'item2'],
- 'dict_arg': {'key1': 'value1', 'key2': 'value2'},
- 'list_of_bool': [True, False]
-}"""
-
- response = build_python_example(tool_fully_qualified_name, input_map, TOOL_CALL_EXAMPLE_PY)
- assert response == TOOL_CALL_EXAMPLE_PY.format(
- tool_fully_qualified_name=tool_fully_qualified_name,
- input_map=input_map_str,
- )
diff --git a/libs/tests/cli/toolkit_docs/test_docs_builder_utils.py b/libs/tests/cli/toolkit_docs/test_docs_builder_utils.py
deleted file mode 100644
index c04cdb40..00000000
--- a/libs/tests/cli/toolkit_docs/test_docs_builder_utils.py
+++ /dev/null
@@ -1,175 +0,0 @@
-from types import ModuleType
-from unittest.mock import MagicMock, patch
-
-import pytest
-from arcade_cli.toolkit_docs.utils import (
- clean_fully_qualified_name,
- get_toolkit_auth_type,
- is_well_known_provider,
- pascal_to_snake_case,
- read_toolkit_metadata,
-)
-from arcade_core.auth import Asana, AuthProviderType, Google, OAuth2, Slack
-from arcade_core.schema import ToolAuthRequirement
-
-
-@patch("arcade_cli.toolkit_docs.utils.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 = """
-[build-system]
-requires = [ "hatchling",]
-build-backend = "hatchling.build"
-
-[project]
-name = "arcade_jira"
-version = "0.1.2"
-description = "Arcade.dev LLM tools for interacting with Atlassian Jira"
-requires-python = ">=3.10"
-dependencies = [
- "arcade-tdk>=2.0.0,<3.0.0",
- "httpx>=0.27.2,<1.0.0",
-]
-[[project.authors]]
-name = "Arcade"
-email = "dev@arcade.dev"
-
-[project.optional-dependencies]
-dev = [
- "arcade-mcp[evals]>=2.0.0,<3.0.0",
- "arcade-serve>=2.0.0,<3.0.0",
- "pytest>=8.3.0,<8.4.0",
- "pytest-cov>=4.0.0,<4.1.0",
- "pytest-asyncio>=0.24.0,<0.25.0",
- "pytest-mock>=3.11.1,<3.12.0",
- "mypy>=1.5.1,<1.6.0",
- "pre-commit>=3.4.0,<3.5.0",
- "tox>=4.11.1,<4.12.0",
- "ruff>=0.7.4,<0.8.0",
-]
-
-# Use local path sources for arcade libs when working locally
-[tool.uv.sources]
-arcade-mcp = {path = "../../", editable = true}
-arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
-arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
-
-[tool.mypy]
-files = [ "arcade_jira/**/*.py",]
-python_version = "3.10"
-disallow_untyped_defs = "True"
-disallow_any_unimported = "True"
-no_implicit_optional = "True"
-check_untyped_defs = "True"
-warn_return_any = "True"
-warn_unused_ignores = "True"
-show_error_codes = "True"
-ignore_missing_imports = "True"
-
-[tool.pytest.ini_options]
-testpaths = [ "tests",]
-
-[tool.coverage.report]
-skip_empty = true
-
-[tool.hatch.build.targets.wheel]
-packages = [ "arcade_jira",]
- """
-
- # 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"
-
-
-@patch("arcade_cli.toolkit_docs.utils.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 = """
-[build-system]
-requires = [ "hatchling",]
-build-backend = "hatchling.build"
-
-[project]
-version = "0.1.2"
-description = "Arcade.dev LLM tools for interacting with Atlassian Jira"
-requires-python = ">=3.10"
-dependencies = [
- "arcade-tdk>=2.0.0,<3.0.0",
- "httpx>=0.27.2,<1.0.0",
-]
-[[project.authors]]
-name = "Arcade"
-email = "dev@arcade.dev"
- """
-
- # 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):
- read_toolkit_metadata("path/to/toolkits/jira")
-
-
-def test_pascal_to_snake_case():
- assert pascal_to_snake_case("PascalCase") == "pascal_case"
- assert pascal_to_snake_case("PascalCase_abc") == "pascal_case_abc"
-
-
-def test_get_toolkit_auth_type_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():
- from arcade_core.schema import ToolRequirements, ToolSecretRequirement
-
- tool_req = ToolRequirements(authorization=ToolAuthRequirement(provider_type=AuthProviderType.oauth2.value))
- assert get_toolkit_auth_type(tool_req=tool_req) == 'authType="OAuth2"'
-
- tool_req = ToolRequirements(authorization=ToolAuthRequirement(provider_type="another_type"))
- 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():
- assert not is_well_known_provider(provider_id=None, auth_module=MagicMock(spec=ModuleType))
-
-
-def test_is_well_known_provider_matching_provider_id():
- mock_auth_module = MagicMock(spec=ModuleType)
-
- mock_auth_module.OAuth2 = OAuth2
- mock_auth_module.Google = Google
- mock_auth_module.Slack = Slack
-
- assert is_well_known_provider(provider_id=Google().provider_id, auth_module=mock_auth_module)
- assert is_well_known_provider(provider_id=Slack().provider_id, auth_module=mock_auth_module)
- assert not is_well_known_provider(provider_id=Asana().provider_id, auth_module=mock_auth_module)
- assert not is_well_known_provider(provider_id="another_provider", auth_module=mock_auth_module)
-
-
-def test_clean_fully_qualified_name():
- assert clean_fully_qualified_name("Outlook.ListEmails") == "Outlook.ListEmails"
- assert clean_fully_qualified_name("Outlook.ListEmails@1.0.0") == "Outlook.ListEmails"