arcade new <name> --full generates an MCPApp (#787)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Moderate risk because it changes the default `arcade new --full` scaffolding (dependencies, entry points, Makefile workflow, licensing) and removes interactive prompts, which could break existing expectations for generated projects. > > **Overview** > `arcade new --full` scaffolding is simplified to be non-interactive and always generate an `arcade_<name>` package, with derived name variants (title/hyphenated) and updated next-step instructions (`make install/dev/test/lint`). > > The full template is updated to produce a runnable `arcade-mcp-server` `MCPApp` (`__main__.py` + `project.scripts`), switch sample code/tests/evals to a new async Reddit example tool (auth + metadata) using `httpx`, and refresh dev tooling (new `Makefile`, ruff config tweaks, added pytest `conftest.py`). > > Template licensing/metadata is standardized to Arcade proprietary (removes community/official conditionals and the templated README), and the repo version is bumped to `1.12.0`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fd39c9ed9beba068fe85cf96979f04a31a40daa4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
parent
830480de83
commit
4d48bb765d
15 changed files with 198 additions and 300 deletions
|
|
@ -3,9 +3,7 @@ import shutil
|
|||
from datetime import datetime
|
||||
from importlib.metadata import version as get_version
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from arcade_cli.console import console
|
||||
|
|
@ -20,48 +18,10 @@ except Exception as e:
|
|||
ARCADE_MCP_MIN_VERSION = "1.10.0" # Default version if unable to fetch
|
||||
ARCADE_MCP_MAX_VERSION = "2.0.0"
|
||||
|
||||
ARCADE_TDK_MIN_VERSION = "3.6.0"
|
||||
ARCADE_TDK_MAX_VERSION = "4.0.0"
|
||||
ARCADE_SERVE_MIN_VERSION = "3.1.5"
|
||||
ARCADE_SERVE_MAX_VERSION = "4.0.0"
|
||||
ARCADE_MCP_SERVER_MIN_VERSION = "1.17.0"
|
||||
ARCADE_MCP_SERVER_MAX_VERSION = "2.0.0"
|
||||
|
||||
|
||||
def ask_question(question: str, default: Optional[str] = None) -> str:
|
||||
"""
|
||||
Ask a question via input() and return the answer.
|
||||
"""
|
||||
answer = typer.prompt(question, default=default, show_default=False)
|
||||
if not answer and default:
|
||||
return default
|
||||
return str(answer)
|
||||
|
||||
|
||||
def ask_yes_no_question(question: str, default: bool = True) -> bool:
|
||||
"""
|
||||
Ask a yes/no question via input() and return the bool answer.
|
||||
"""
|
||||
default_str = "Y/n" if default else "y/N"
|
||||
answer = typer.prompt(
|
||||
f"{question} ({default_str})", default="y" if default else "n", show_default=False
|
||||
)
|
||||
return answer.lower() in [
|
||||
"y",
|
||||
"y/",
|
||||
"yes",
|
||||
"true",
|
||||
"1",
|
||||
"ye",
|
||||
"yes",
|
||||
"yeah",
|
||||
"yep",
|
||||
"sure",
|
||||
"ok",
|
||||
"yup",
|
||||
]
|
||||
|
||||
|
||||
def render_template(env: Environment, template_string: str, context: dict) -> str:
|
||||
"""Render a template string with the given variables."""
|
||||
template = env.from_string(template_string)
|
||||
|
|
@ -73,9 +33,7 @@ def write_template(path: Path, content: str) -> None:
|
|||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def create_ignore_pattern(
|
||||
include_evals: bool, is_community_or_official_toolkit: bool
|
||||
) -> re.Pattern[str]:
|
||||
def create_ignore_pattern(include_evals: bool) -> re.Pattern[str]:
|
||||
"""Create an ignore pattern based on user preferences."""
|
||||
patterns = [
|
||||
"__pycache__",
|
||||
|
|
@ -96,11 +54,6 @@ def create_ignore_pattern(
|
|||
if not include_evals:
|
||||
patterns.append("evals")
|
||||
|
||||
if not is_community_or_official_toolkit:
|
||||
patterns.extend([".ruff.toml", ".pre-commit-config.yaml", "LICENSE"])
|
||||
else:
|
||||
patterns.extend(["README.md"])
|
||||
|
||||
return re.compile(f"({'|'.join(patterns)})$")
|
||||
|
||||
|
||||
|
|
@ -166,47 +119,21 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
)
|
||||
exit(1)
|
||||
|
||||
toolkit_description = ask_question("Describe what your server will do (optional)", default="")
|
||||
toolkit_author_name = ask_question("Your GitHub username (optional)", default="")
|
||||
while True:
|
||||
toolkit_author_email = ask_question("Your email (optional)", default="")
|
||||
if toolkit_author_email == "" or re.match(r"[^@ ]+@[^@ ]+\.[^@ ]+", toolkit_author_email):
|
||||
break
|
||||
console.print(
|
||||
"[red]Invalid email format. Please enter a valid email address or leave it empty.[/red]"
|
||||
)
|
||||
include_evals = ask_yes_no_question(
|
||||
"Do you want an evals directory created for you?", default=True
|
||||
)
|
||||
|
||||
cwd = Path.cwd()
|
||||
# TODO: this detection mechanism works only for people that didn't change the
|
||||
# name of the repo, a better detection method is required here
|
||||
is_community_toolkit = False
|
||||
if cwd.name == "toolkits" and cwd.parent.name == "arcade-mcp":
|
||||
prompt = (
|
||||
"Is your server a community contribution (to be merged into "
|
||||
"\x1b]8;;https://github.com/ArcadeAI/arcade-mcp\x1b\\ArcadeAI/arcade-mcp\x1b]8;;\x1b\\ repo)?"
|
||||
)
|
||||
is_community_toolkit = ask_yes_no_question(prompt, default=True)
|
||||
|
||||
is_official_toolkit = cwd.name == "toolkits" and cwd.parent.name == "tools"
|
||||
toolkit_name_title = toolkit_name.replace("_", " ").title()
|
||||
toolkit_name_hyphenated = toolkit_name.replace("_", "-")
|
||||
toolkit_description = f"Arcade.dev tools for interacting with {toolkit_name_title}"
|
||||
|
||||
context = {
|
||||
"package_name": "arcade_" + toolkit_name if is_community_toolkit else toolkit_name,
|
||||
"package_name": "arcade_" + toolkit_name,
|
||||
"toolkit_name": toolkit_name,
|
||||
"toolkit_name_title": toolkit_name_title,
|
||||
"toolkit_name_hyphenated": toolkit_name_hyphenated,
|
||||
"toolkit_description": toolkit_description,
|
||||
"toolkit_author_name": toolkit_author_name,
|
||||
"toolkit_author_email": toolkit_author_email,
|
||||
"arcade_tdk_min_version": ARCADE_TDK_MIN_VERSION,
|
||||
"arcade_tdk_max_version": ARCADE_TDK_MAX_VERSION,
|
||||
"arcade_serve_min_version": ARCADE_SERVE_MIN_VERSION,
|
||||
"arcade_serve_max_version": ARCADE_SERVE_MAX_VERSION,
|
||||
"arcade_mcp_server_min_version": ARCADE_MCP_SERVER_MIN_VERSION,
|
||||
"arcade_mcp_server_max_version": ARCADE_MCP_SERVER_MAX_VERSION,
|
||||
"arcade_mcp_min_version": ARCADE_MCP_MIN_VERSION,
|
||||
"arcade_mcp_max_version": ARCADE_MCP_MAX_VERSION,
|
||||
"creation_year": datetime.now().year,
|
||||
"is_community_toolkit": is_community_toolkit,
|
||||
"is_official_toolkit": is_official_toolkit,
|
||||
}
|
||||
|
||||
template_directory = get_full_template_directory() / "{{ toolkit_name }}"
|
||||
|
|
@ -214,12 +141,10 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
env = Environment(
|
||||
loader=FileSystemLoader(str(template_directory)),
|
||||
autoescape=select_autoescape(["html", "xml"]),
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
|
||||
# Create dynamic ignore pattern based on user preferences
|
||||
ignore_pattern = create_ignore_pattern(
|
||||
include_evals, is_community_toolkit or is_official_toolkit
|
||||
)
|
||||
ignore_pattern = create_ignore_pattern(include_evals=True)
|
||||
|
||||
try:
|
||||
create_package(env, template_directory, toolkit_directory, context, ignore_pattern)
|
||||
|
|
@ -228,23 +153,16 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
)
|
||||
console.print("\nNext steps:", style="bold")
|
||||
console.print(f" 1. cd {toolkit_directory / toolkit_name}")
|
||||
console.print(" 2. make install")
|
||||
console.print(" 3. make dev # serve with MCP and worker endpoints")
|
||||
console.print(" 4. make test # run tests")
|
||||
console.print(" 5. make lint # run linting")
|
||||
console.print("")
|
||||
console.print(" 2. Run the server (choose one transport):", style="dim")
|
||||
console.print(" - stdio: uv run server.py")
|
||||
console.print(" - http: uv run server.py --transport http --port 8000")
|
||||
console.print("")
|
||||
create_deployment(toolkit_directory, toolkit_name)
|
||||
except Exception:
|
||||
remove_toolkit(toolkit_directory, toolkit_name)
|
||||
raise
|
||||
|
||||
|
||||
def create_deployment(toolkit_directory: Path, toolkit_name: str) -> None:
|
||||
# No longer create worker.toml for MCP servers
|
||||
# The server.py file handles all configuration
|
||||
pass
|
||||
|
||||
|
||||
def create_new_toolkit_minimal(output_directory: str, toolkit_name: str) -> None:
|
||||
"""Create a new toolkit from a template with user input."""
|
||||
toolkit_directory = Path(output_directory)
|
||||
|
|
@ -274,9 +192,10 @@ def create_new_toolkit_minimal(output_directory: str, toolkit_name: str) -> None
|
|||
env = Environment(
|
||||
loader=FileSystemLoader(str(template_directory)),
|
||||
autoescape=select_autoescape(["html", "xml"]),
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
|
||||
ignore_pattern = create_ignore_pattern(False, False)
|
||||
ignore_pattern = create_ignore_pattern(False)
|
||||
|
||||
try:
|
||||
create_package(env, template_directory, toolkit_directory, context, ignore_pattern)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ select = [
|
|||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"**/tests/*" = ["S101"]
|
||||
"*" = ["TRY003", "B904"]
|
||||
"**/tests/*" = ["S101", "E501"]
|
||||
"**/evals/*" = ["S101", "E501"]
|
||||
|
||||
[format]
|
||||
preview = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{% if is_official_toolkit -%}
|
||||
# The Arcade Software License Agreement
|
||||
|
||||
- Version 1.0
|
||||
|
|
@ -6,7 +5,7 @@
|
|||
|
||||
---
|
||||
|
||||
This software and associated documentation (collectively, the “Software”) are the intellectual property of Arcade Technologies, Inc. (“Arcade”). All rights are reserved.
|
||||
This software and associated documentation (collectively, the "Software") are the intellectual property of Arcade Technologies, Inc. ("Arcade"). All rights are reserved.
|
||||
|
||||
1. License Grant
|
||||
|
||||
|
|
@ -29,32 +28,8 @@ Arcade retains all right, title, and interest in and to the Software, including
|
|||
|
||||
5. Disclaimer of Warranty
|
||||
|
||||
The Software is provided “as is” without warranty of any kind. Arcade disclaims all warranties, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, and noninfringement.
|
||||
The Software is provided "as is" without warranty of any kind. Arcade disclaims all warranties, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
6. Limitation of Liability
|
||||
|
||||
In no event shall Arcade be liable for any damages arising out of or in connection with the use or performance of the Software, whether in an action of contract, tort (including negligence), or otherwise.
|
||||
{% endif -%}
|
||||
{% if is_community_toolkit -%}
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025, Arcade AI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
{% endif -%}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,31 @@
|
|||
.PHONY: help
|
||||
.PHONY: install build test lint dev
|
||||
|
||||
help:
|
||||
@echo "🛠️ github Commands:\n"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
TOOLKIT := $(shell basename $(CURDIR))
|
||||
|
||||
.PHONY: install
|
||||
install: ## Install the uv environment and install all packages with dependencies
|
||||
@echo "🚀 Creating virtual environment and installing all packages using uv"
|
||||
@uv sync --active --all-extras --no-sources
|
||||
@if [ -f .pre-commit-config.yaml ]; then uv run --no-sources pre-commit install; fi
|
||||
@echo "✅ All packages and dependencies installed via uv"
|
||||
help: ## Show this help message
|
||||
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*##"}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
{% if is_community_toolkit or is_official_toolkit -%}
|
||||
.PHONY: install-local
|
||||
install-local: ## Install the uv environment and install all packages with dependencies with local Arcade sources
|
||||
@echo "🚀 Creating virtual environment and installing all packages using uv"
|
||||
@uv sync --active --all-extras
|
||||
@if [ -f .pre-commit-config.yaml ]; then uv run pre-commit install; fi
|
||||
@echo "✅ All packages and dependencies installed via uv"
|
||||
{% endif -%}
|
||||
install: ## Install dependencies, then overlay any ../.local-overrides
|
||||
uv sync --all-extras
|
||||
uv run pre-commit install
|
||||
@if [ -f ../.local-overrides ]; then \
|
||||
while IFS= read -r pkg || [ -n "$$pkg" ]; do \
|
||||
case "$$pkg" in \#*|"") continue ;; esac; \
|
||||
echo "Applying local override: $$pkg"; \
|
||||
uv pip install -e "$$pkg"; \
|
||||
done < ../.local-overrides; \
|
||||
fi
|
||||
|
||||
.PHONY: build
|
||||
build: clean-build ## Build wheel file using poetry
|
||||
@echo "🚀 Creating wheel file"
|
||||
build: ## Build wheel
|
||||
rm -rf dist
|
||||
uv build
|
||||
|
||||
.PHONY: clean-build
|
||||
clean-build: ## clean build artifacts
|
||||
@echo "🗑️ Cleaning dist directory"
|
||||
rm -rf dist
|
||||
test: ## Run tests with coverage
|
||||
uv run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
|
||||
|
||||
.PHONY: test
|
||||
test: ## Test the code with pytest
|
||||
@echo "🚀 Testing code: Running pytest"
|
||||
@uv run --no-sources pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
|
||||
lint: ## Run linting and type checking
|
||||
uv run pre-commit run -a
|
||||
uv run mypy --config-file=pyproject.toml
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: ## Generate coverage report
|
||||
@echo "coverage report"
|
||||
@uv run --no-sources coverage report
|
||||
@echo "Generating coverage report"
|
||||
@uv run --no-sources coverage html
|
||||
|
||||
.PHONY: bump-version
|
||||
bump-version: ## Bump the version in the pyproject.toml file by a patch version
|
||||
@echo "🚀 Bumping version in pyproject.toml"
|
||||
uv version --no-sources --bump patch
|
||||
|
||||
.PHONY: check
|
||||
check: ## Run code quality tools.
|
||||
@if [ -f .pre-commit-config.yaml ]; then\
|
||||
echo "🚀 Linting code: Running pre-commit";\
|
||||
uv run --no-sources pre-commit run -a;\
|
||||
fi
|
||||
@echo "🚀 Static type checking: Running mypy"
|
||||
@uv run --no-sources mypy --config-file=pyproject.toml
|
||||
dev: ## Run toolkit locally as MCP server
|
||||
ARCADE_WORKER_SECRET=dev uv run arcade_$(TOOLKIT) http
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<img
|
||||
src="https://docs.arcade.dev/images/logo/arcade-logo.png"
|
||||
style="width: 250px;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center; margin-bottom: 8px;">
|
||||
{% if toolkit_author_name -%}
|
||||
<img src="https://img.shields.io/github/v/release/{{ toolkit_author_name }}/{{ toolkit_name }}" alt="GitHub release" style="margin: 0 2px;">
|
||||
{% endif -%}
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python version" style="margin: 0 2px;">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License" style="margin: 0 2px;">
|
||||
<img src="https://img.shields.io/pypi/v/{{ package_name }}" alt="PyPI version" style="margin: 0 2px;">
|
||||
</div>
|
||||
{% if toolkit_author_name -%}
|
||||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<a href="https://github.com/{{ toolkit_author_name }}/{{ toolkit_name }}" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/{{ toolkit_author_name }}/{{ toolkit_name }}" alt="GitHub stars" style="margin: 0 2px;">
|
||||
</a>
|
||||
<a href="https://github.com/{{ toolkit_author_name }}/{{ toolkit_name }}/fork" target="_blank">
|
||||
<img src="https://img.shields.io/github/forks/{{ toolkit_author_name }}/{{ toolkit_name }}" alt="GitHub forks" style="margin: 0 2px;">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
# Arcade {{ toolkit_name }} Toolkit
|
||||
{% if toolkit_description -%}
|
||||
{{ toolkit_description }}
|
||||
{% endif -%}
|
||||
## Features
|
||||
|
||||
- The {{ toolkit_name }} toolkit does not have any features yet.
|
||||
|
||||
## Development
|
||||
|
||||
Read the docs on how to create a toolkit [here](https://docs.arcade.dev/en/guides/create-tools/tool-basics/build-mcp-server)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context():
|
||||
context = AsyncMock()
|
||||
context.authorization.token = "fake-token" # noqa: S105
|
||||
context.get_auth_token_or_empty = MagicMock(return_value="fake-token")
|
||||
return context
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
from arcade_tdk import ToolCatalog
|
||||
from arcade_core import ToolCatalog
|
||||
from arcade_evals import (
|
||||
EvalRubric,
|
||||
EvalSuite,
|
||||
ExpectedToolCall,
|
||||
tool_eval,
|
||||
)
|
||||
from arcade_evals.critic import SimilarityCritic
|
||||
|
||||
import {{ package_name }}
|
||||
from {{ package_name }}.tools.hello import say_hello
|
||||
from {{ package_name }}.tools.sample import get_my_reddit_profile
|
||||
|
||||
# Evaluation rubric
|
||||
rubric = EvalRubric(
|
||||
|
|
@ -34,17 +33,10 @@ def {{ toolkit_name }}_eval_suite() -> EvalSuite:
|
|||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Saying hello",
|
||||
user_message="He's actually right here, say hi to him!",
|
||||
expected_tool_calls=[ExpectedToolCall(func=say_hello, args={"name": "John Doe"})],
|
||||
name="Get my Reddit profile",
|
||||
user_message="What is my Reddit username and karma?",
|
||||
expected_tool_calls=[ExpectedToolCall(func=get_my_reddit_profile, args={})],
|
||||
rubric=rubric,
|
||||
critics=[
|
||||
SimilarityCritic(critic_field="name", weight=0.5),
|
||||
],
|
||||
additional_messages=[
|
||||
{"role": "user", "content": "My friend's name is John Doe."},
|
||||
{"role": "assistant", "content": "It is great that you have a friend named John Doe!"},
|
||||
],
|
||||
)
|
||||
|
||||
return suite
|
||||
|
|
|
|||
|
|
@ -5,56 +5,35 @@ build-backend = "hatchling.build"
|
|||
[project]
|
||||
name = "{{ package_name }}"
|
||||
version = "0.1.0"
|
||||
{% if toolkit_description -%}
|
||||
description = "{{ toolkit_description }}"
|
||||
{% endif -%}
|
||||
license = {text = "Proprietary - Arcade Software License Agreement v1.0"}
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"arcade-tdk>={{ arcade_tdk_min_version }},<{{ arcade_tdk_max_version}}",
|
||||
"arcade-mcp-server>={{ arcade_mcp_server_min_version }},<{{ arcade_mcp_server_max_version }}",
|
||||
"httpx>=0.27.0,<1.0.0",
|
||||
]
|
||||
{% if toolkit_author_name or toolkit_author_email -%}
|
||||
|
||||
[project.scripts]
|
||||
arcade-{{ toolkit_name_hyphenated }} = "{{ package_name }}.__main__:main"
|
||||
{{ package_name }} = "{{ package_name }}.__main__:main"
|
||||
|
||||
[[project.authors]]
|
||||
{% if toolkit_author_name -%}
|
||||
name = "{{ toolkit_author_name }}"
|
||||
{% endif -%}
|
||||
{% if toolkit_author_email -%}
|
||||
email = "{{ toolkit_author_email }}"
|
||||
{% endif -%}
|
||||
{% endif %}
|
||||
name = "Arcade"
|
||||
email = "dev@arcade.dev"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"arcade-mcp[evals]>={{ arcade_mcp_min_version }},<{{ arcade_mcp_max_version }}",
|
||||
"arcade-serve>={{ arcade_serve_min_version }},<{{ arcade_serve_max_version }}",
|
||||
"pytest>=8.3.0,<8.4.0",
|
||||
"pytest-cov>=4.0.0,<4.1.0",
|
||||
"pytest-mock>=3.11.1,<3.12.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",
|
||||
]
|
||||
|
||||
# Tell Arcade.dev that this package is a toolkit
|
||||
[project.entry-points.arcade_toolkits]
|
||||
toolkit_name = "{{ package_name }}"
|
||||
|
||||
{% if is_community_toolkit -%}
|
||||
# Use local path sources for arcade libs when working locally
|
||||
[tool.uv.sources]
|
||||
arcade-mcp = { path = "../../", editable = true }
|
||||
arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
|
||||
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
|
||||
{% endif -%}
|
||||
{% if is_official_toolkit -%}
|
||||
# Use local path sources for arcade libs when working locally
|
||||
[tool.uv.sources]
|
||||
arcade-mcp = { path = "../../../arcade-mcp", editable = true }
|
||||
arcade-serve = { path = "../../../arcade-mcp/libs/arcade-serve/", editable = true }
|
||||
arcade-tdk = { path = "../../../arcade-mcp/libs/arcade-tdk/", editable = true }
|
||||
{% endif -%}
|
||||
|
||||
[tool.mypy]
|
||||
files = [ "{{ package_name }}/**/*.py",]
|
||||
python_version = "3.10"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from arcade_tdk.errors import ToolExecutionError
|
||||
|
||||
from {{ package_name }}.tools.hello import say_hello
|
||||
from {{ package_name }}.tools.sample import RedditUserProfile, get_my_reddit_profile
|
||||
|
||||
|
||||
def test_hello() -> None:
|
||||
assert say_hello("developer") == "Hello, developer!"
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_my_reddit_profile(mock_context) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"name": "test_user",
|
||||
"comment_karma": 100,
|
||||
"link_karma": 200,
|
||||
}
|
||||
|
||||
with patch("{{ package_name }}.tools.sample.httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||
result = await get_my_reddit_profile(mock_context)
|
||||
|
||||
def test_hello_raises_error() -> None:
|
||||
with pytest.raises(ToolExecutionError):
|
||||
say_hello(1)
|
||||
assert result == RedditUserProfile(
|
||||
username="test_user",
|
||||
comment_karma=100,
|
||||
link_karma=200,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from {{ package_name }}.tools.sample import RedditUserProfile, get_my_reddit_profile
|
||||
|
||||
__all__ = ["RedditUserProfile", "get_my_reddit_profile"]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import sys
|
||||
from typing import cast
|
||||
|
||||
from arcade_mcp_server import MCPApp
|
||||
from arcade_mcp_server.mcp_app import TransportType
|
||||
|
||||
import {{ package_name }}
|
||||
|
||||
app = MCPApp(
|
||||
name="{{ toolkit_name_title }}",
|
||||
instructions=(
|
||||
"Use this server when you need to interact with {{ toolkit_name_title }}"
|
||||
),
|
||||
)
|
||||
|
||||
app.add_tools_from_module({{ package_name }})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
transport = sys.argv[1] if len(sys.argv) > 1 else "stdio"
|
||||
host = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1"
|
||||
port = int(sys.argv[3]) if len(sys.argv) > 3 else 8000
|
||||
|
||||
app.run(transport=cast(TransportType, transport), host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from typing import Annotated
|
||||
|
||||
from arcade_tdk import tool
|
||||
|
||||
|
||||
@tool
|
||||
def say_hello(name: Annotated[str, "The name of the person to greet"]) -> str:
|
||||
"""Say a greeting!"""
|
||||
|
||||
return "Hello, " + name + "!"
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
from typing import Annotated, TypedDict
|
||||
|
||||
import httpx
|
||||
from arcade_mcp_server import Context, tool
|
||||
from arcade_mcp_server.auth import Reddit
|
||||
from arcade_mcp_server.metadata import (
|
||||
Behavior,
|
||||
Classification,
|
||||
Operation,
|
||||
ServiceDomain,
|
||||
ToolMetadata,
|
||||
)
|
||||
|
||||
REDDIT_API_URL = "https://oauth.reddit.com"
|
||||
|
||||
|
||||
class RedditUserProfile(TypedDict, total=True):
|
||||
username: str
|
||||
comment_karma: int | None
|
||||
link_karma: int | None
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Reddit(scopes=["identity"]),
|
||||
metadata=ToolMetadata(
|
||||
classification=Classification(
|
||||
service_domains=[ServiceDomain.SOCIAL_MEDIA],
|
||||
),
|
||||
behavior=Behavior(
|
||||
operations=[Operation.READ],
|
||||
read_only=True,
|
||||
destructive=False,
|
||||
idempotent=True,
|
||||
open_world=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
async def get_my_reddit_profile(
|
||||
context: Context,
|
||||
include_karma: Annotated[
|
||||
bool, "Whether to include karma breakdown in the response"
|
||||
] = True,
|
||||
) -> Annotated[RedditUserProfile, "The authenticated user's Reddit profile"]:
|
||||
"""Get the Reddit profile of the authenticated user."""
|
||||
|
||||
token = context.get_auth_token_or_empty()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{REDDIT_API_URL}/api/v1/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
profile = RedditUserProfile(
|
||||
username=data["name"],
|
||||
comment_karma=data.get("comment_karma", None) if include_karma else None,
|
||||
link_karma=data.get("link_karma", None) if include_karma else None,
|
||||
)
|
||||
|
||||
return profile
|
||||
|
|
@ -1,42 +1,33 @@
|
|||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from arcade_cli.new import create_new_toolkit, create_new_toolkit_minimal
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
def test_create_new_toolkit_prints_next_steps(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_create_new_toolkit_prints_next_steps(tmp_path: Path) -> None:
|
||||
"""create_new_toolkit (full template) should print numbered next steps."""
|
||||
output_dir = tmp_path / "full_test"
|
||||
output_dir.mkdir()
|
||||
|
||||
# Use a cwd that does not trigger community/official toolkit prompts
|
||||
fake_cwd = tmp_path / "cwd"
|
||||
fake_cwd.mkdir()
|
||||
monkeypatch.chdir(fake_cwd)
|
||||
buf = StringIO()
|
||||
test_console = Console(file=buf, force_terminal=False)
|
||||
import arcade_cli.new as new_mod
|
||||
|
||||
# Mock prompts: description, author, email, evals (yes)
|
||||
with patch("arcade_cli.new.typer.prompt", side_effect=["", "", "", "y"]):
|
||||
buf = StringIO()
|
||||
test_console = Console(file=buf, force_terminal=False)
|
||||
import arcade_cli.new as new_mod
|
||||
|
||||
orig = new_mod.console
|
||||
new_mod.console = test_console
|
||||
try:
|
||||
create_new_toolkit(str(output_dir), "my_server")
|
||||
finally:
|
||||
new_mod.console = orig
|
||||
orig = new_mod.console
|
||||
new_mod.console = test_console
|
||||
try:
|
||||
create_new_toolkit(str(output_dir), "my_server")
|
||||
finally:
|
||||
new_mod.console = orig
|
||||
|
||||
output = buf.getvalue()
|
||||
assert "Next steps:" in output
|
||||
assert "1. cd " in output
|
||||
assert "2. Run the server (choose one transport):" in output
|
||||
assert "- stdio: uv run server.py" in output
|
||||
assert "- http: uv run server.py --transport http --port 8000" in output
|
||||
assert "uv run server.py" in output
|
||||
assert "make install" in output
|
||||
assert "make dev" in output
|
||||
assert "make test" in output
|
||||
assert "my_server" in output
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "arcade-mcp"
|
||||
version = "1.11.2"
|
||||
version = "1.12.0"
|
||||
description = "Arcade.dev - Tool Calling platform for Agents"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue