Add ToolMetadata to OSS toolkits (#776)

Resolves TOO-388


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Primarily metadata/dependency additions with no changes to core tool
execution paths; risk is limited to potential packaging/import issues
from the new `arcade-mcp-server` dependency.
> 
> **Overview**
> Adds `ToolMetadata` to tool decorators across the Bright Data,
ClickHouse, MongoDB, Postgres, LinkedIn, Zendesk, and Math toolkits,
specifying *behavior* (read-only/idempotency/destructive/open-world)
and, where applicable, *service domain* classification.
> 
> Updates each toolkit package to depend on `arcade-mcp-server` (plus
local `uv` source wiring) and bumps toolkit versions accordingly; minor
`__all__` ordering tweaks in Math/Zendesk are included.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3bde3a061194e1d1b6a4e8a2ebd608b17984db4f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Eric Gustin 2026-02-25 09:41:41 -08:00 committed by GitHub
parent fe8ddfd500
commit 5228c75dc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 625 additions and 67 deletions

View file

@ -5,6 +5,13 @@ from typing import Annotated, Any, cast
import requests import requests
from arcade_core.errors import RetryableToolError from arcade_core.errors import RetryableToolError
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_brightdata.bright_data_client import BrightDataClient from arcade_brightdata.bright_data_client import BrightDataClient
@ -51,7 +58,21 @@ class SourceType(str, Enum):
YOUTUBE_VIDEOS = "youtube_videos" YOUTUBE_VIDEOS = "youtube_videos"
@tool(requires_secrets=["BRIGHTDATA_API_KEY", "BRIGHTDATA_ZONE"]) @tool(
requires_secrets=["BRIGHTDATA_API_KEY", "BRIGHTDATA_ZONE"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.WEB_SCRAPING],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
def scrape_as_markdown( def scrape_as_markdown(
context: ToolContext, context: ToolContext,
url: Annotated[str, "URL to scrape"], url: Annotated[str, "URL to scrape"],
@ -71,7 +92,21 @@ def scrape_as_markdown(
return client.make_request(payload) return client.make_request(payload)
@tool(requires_secrets=["BRIGHTDATA_API_KEY", "BRIGHTDATA_ZONE"]) @tool(
requires_secrets=["BRIGHTDATA_API_KEY", "BRIGHTDATA_ZONE"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.WEB_SCRAPING],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
def search_engine( # noqa: C901 def search_engine( # noqa: C901
context: ToolContext, context: ToolContext,
query: Annotated[str, "Search query"], query: Annotated[str, "Search query"],
@ -167,7 +202,21 @@ def search_engine( # noqa: C901
return client.make_request(payload) return client.make_request(payload)
@tool(requires_secrets=["BRIGHTDATA_API_KEY"]) @tool(
requires_secrets=["BRIGHTDATA_API_KEY"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.WEB_SCRAPING],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=False,
open_world=True,
),
),
)
def web_data_feed( def web_data_feed(
context: ToolContext, context: ToolContext,
source_type: Annotated[SourceType, "Type of data source"], source_type: Annotated[SourceType, "Type of data source"],

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_brightdata" name = "arcade_brightdata"
version = "0.2.0" version = "0.3.0"
description = "Search, Crawl and Scrape any site, at scale, without getting blocked" description = "Search, Crawl and Scrape any site, at scale, without getting blocked"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"requests>=2.32.5", "requests>=2.32.5",
] ]
[[project.authors]] [[project.authors]]
@ -49,6 +50,7 @@ ignore_missing_imports = "True"
arcade-mcp = { path = "../../", editable = true } arcade-mcp = { path = "../../", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = [ "tests",] testpaths = [ "tests",]

View file

@ -1,12 +1,24 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError from arcade_tdk.errors import RetryableToolError
from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
@tool(requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_schemas( async def discover_schemas(
context: ToolContext, context: ToolContext,
) -> list[str]: ) -> list[str]:
@ -18,7 +30,18 @@ async def discover_schemas(
return ["default"] return ["default"]
@tool(requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_databases( async def discover_databases(
context: ToolContext, context: ToolContext,
) -> list[str]: ) -> list[str]:
@ -30,7 +53,18 @@ async def discover_databases(
return databases return databases
@tool(requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_tables( async def discover_tables(
context: ToolContext, context: ToolContext,
) -> list[str]: ) -> list[str]:
@ -45,7 +79,18 @@ async def discover_tables(
return tables return tables
@tool(requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def get_table_schema( async def get_table_schema(
context: ToolContext, context: ToolContext,
schema_name: Annotated[str, "The schema to get the table schema of"], schema_name: Annotated[str, "The schema to get the table schema of"],
@ -62,7 +107,18 @@ async def get_table_schema(
return await _get_table_schema(client, "default", table_name) return await _get_table_schema(client, "default", table_name)
@tool(requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["CLICKHOUSE_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def execute_select_query( async def execute_select_query(
context: ToolContext, context: ToolContext,
select_clause: Annotated[ select_clause: Annotated[

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_clickhouse" name = "arcade_clickhouse"
version = "0.1.1" version = "0.2.0"
description = "Tools to query and explore a ClickHouse database" description = "Tools to query and explore a ClickHouse database"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"clickhouse-connect>=0.7.0", "clickhouse-connect>=0.7.0",
"pydantic>=2.11.7", "pydantic>=2.11.7",
"sqlalchemy>=2.0.41", "sqlalchemy>=2.0.41",
@ -41,6 +42,7 @@ dev = [
arcade-mcp = { path = "../../", editable = true } arcade-mcp = { path = "../../", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]

View file

@ -1,5 +1,12 @@
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import LinkedIn from arcade_tdk.auth import LinkedIn
from arcade_tdk.errors import ToolExecutionError from arcade_tdk.errors import ToolExecutionError
@ -10,7 +17,19 @@ from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linked
@tool( @tool(
requires_auth=LinkedIn( requires_auth=LinkedIn(
scopes=["w_member_social"], scopes=["w_member_social"],
) ),
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.SOCIAL_MEDIA],
),
behavior=Behavior(
operations=[Operation.CREATE],
read_only=False,
destructive=False,
idempotent=False,
open_world=True,
),
),
) )
async def create_text_post( async def create_text_post(
context: ToolContext, context: ToolContext,

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_linkedin" name = "arcade_linkedin"
version = "0.1.14" version = "0.2.0"
description = "Arcade.dev LLM tools for LinkedIn" description = "Arcade.dev LLM tools for LinkedIn"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"httpx>=0.27.2,<1.0.0", "httpx>=0.27.2,<1.0.0",
] ]
[[project.authors]] [[project.authors]]
@ -34,6 +35,7 @@ dev = [
arcade-mcp = {path = "../../", editable = true} arcade-mcp = {path = "../../", editable = true}
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]
files = [ "arcade_linkedin/**/*.py",] files = [ "arcade_linkedin/**/*.py",]

View file

@ -39,27 +39,27 @@ from arcade_math.tools.trigonometry import (
) )
__all__ = [ __all__ = [
"add",
"subtract",
"multiply",
"divide",
"sum_list",
"sum_range",
"mod",
"log",
"power",
"abs_val", "abs_val",
"add",
"avg",
"ceil",
"deg_to_rad",
"divide",
"factorial", "factorial",
"sqrt", "floor",
"gcd",
"generate_random_float", "generate_random_float",
"generate_random_int", "generate_random_int",
"gcd",
"lcm", "lcm",
"ceil", "log",
"floor",
"round_num",
"avg",
"median", "median",
"deg_to_rad", "mod",
"multiply",
"power",
"rad_to_deg", "rad_to_deg",
"round_num",
"sqrt",
"subtract",
"sum_list",
"sum_range",
] ]

View file

@ -2,12 +2,22 @@ import decimal
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def add( def add(
a: Annotated[str, "The first number as a string"], a: Annotated[str, "The first number as a string"],
b: Annotated[str, "The second number as a string"], b: Annotated[str, "The second number as a string"],
@ -21,7 +31,16 @@ def add(
return str(a_decimal + b_decimal) return str(a_decimal + b_decimal)
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def subtract( def subtract(
a: Annotated[str, "The first number as a string"], a: Annotated[str, "The first number as a string"],
b: Annotated[str, "The second number as a string"], b: Annotated[str, "The second number as a string"],
@ -35,7 +54,16 @@ def subtract(
return str(a_decimal - b_decimal) return str(a_decimal - b_decimal)
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def multiply( def multiply(
a: Annotated[str, "The first number as a string"], a: Annotated[str, "The first number as a string"],
b: Annotated[str, "The second number as a string"], b: Annotated[str, "The second number as a string"],
@ -49,7 +77,16 @@ def multiply(
return str(a_decimal * b_decimal) return str(a_decimal * b_decimal)
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def divide( def divide(
a: Annotated[str, "The first number as a string"], a: Annotated[str, "The first number as a string"],
b: Annotated[str, "The second number as a string"], b: Annotated[str, "The second number as a string"],
@ -63,7 +100,16 @@ def divide(
return str(a_decimal / b_decimal) return str(a_decimal / b_decimal)
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def sum_list( def sum_list(
numbers: Annotated[list[str], "The list of numbers as strings"], numbers: Annotated[list[str], "The list of numbers as strings"],
) -> Annotated[str, "The sum of the numbers in the list as a string"]: ) -> Annotated[str, "The sum of the numbers in the list as a string"]:
@ -74,7 +120,16 @@ def sum_list(
return str(sum([Decimal(n) for n in numbers])) return str(sum([Decimal(n) for n in numbers]))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def sum_range( def sum_range(
start: Annotated[str, "The start of the range to sum as a string"], start: Annotated[str, "The start of the range to sum as a string"],
end: Annotated[str, "The end of the range to sum as a string"], end: Annotated[str, "The end of the range to sum as a string"],
@ -85,7 +140,16 @@ def sum_range(
return str(sum(list(range(int(start), int(end) + 1)))) return str(sum(list(range(int(start), int(end) + 1))))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def mod( def mod(
a: Annotated[str, "The dividend as a string"], a: Annotated[str, "The dividend as a string"],
b: Annotated[str, "The divisor as a string"], b: Annotated[str, "The divisor as a string"],

View file

@ -3,12 +3,22 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def log( def log(
a: Annotated[str, "The number to take the logarithm of as a string"], a: Annotated[str, "The number to take the logarithm of as a string"],
base: Annotated[str, "The logarithmic base as a string"], base: Annotated[str, "The logarithmic base as a string"],
@ -20,7 +30,16 @@ def log(
return str(math.log(Decimal(a), Decimal(base))) return str(math.log(Decimal(a), Decimal(base)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def power( def power(
a: Annotated[str, "The base number as a string"], a: Annotated[str, "The base number as a string"],
b: Annotated[str, "The exponent as a string"], b: Annotated[str, "The exponent as a string"],

View file

@ -3,12 +3,22 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def abs_val( def abs_val(
a: Annotated[str, "The number as a string"], a: Annotated[str, "The number as a string"],
) -> Annotated[str, "The absolute value of the number as a string"]: ) -> Annotated[str, "The absolute value of the number as a string"]:
@ -19,7 +29,16 @@ def abs_val(
return str(abs(Decimal(a))) return str(abs(Decimal(a)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def factorial( def factorial(
a: Annotated[str, "The non-negative integer to compute the factorial for as a string"], a: Annotated[str, "The non-negative integer to compute the factorial for as a string"],
) -> Annotated[str, "The factorial of the number as a string"]: ) -> Annotated[str, "The factorial of the number as a string"]:
@ -30,7 +49,16 @@ def factorial(
return str(math.factorial(int(a))) return str(math.factorial(int(a)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def sqrt( def sqrt(
a: Annotated[str, "The number to square root as a string"], a: Annotated[str, "The number to square root as a string"],
) -> Annotated[str, "The square root of the number as a string"]: ) -> Annotated[str, "The square root of the number as a string"]:

View file

@ -1,10 +1,20 @@
import random import random
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=False,
open_world=False,
),
),
)
def generate_random_int( def generate_random_int(
min_value: Annotated[str, "The minimum value of the random integer as a string"], min_value: Annotated[str, "The minimum value of the random integer as a string"],
max_value: Annotated[str, "The maximum value of the random integer as a string"], max_value: Annotated[str, "The maximum value of the random integer as a string"],
@ -21,7 +31,16 @@ def generate_random_int(
return str(random.randint(int(min_value), int(max_value))) # noqa: S311 return str(random.randint(int(min_value), int(max_value))) # noqa: S311
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=False,
open_world=False,
),
),
)
def generate_random_float( def generate_random_float(
min_value: Annotated[str, "The minimum value of the random float as a string"], min_value: Annotated[str, "The minimum value of the random float as a string"],
max_value: Annotated[str, "The maximum value of the random float as a string"], max_value: Annotated[str, "The maximum value of the random float as a string"],

View file

@ -1,10 +1,20 @@
import math import math
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def gcd( def gcd(
a: Annotated[str, "First integer as a string"], a: Annotated[str, "First integer as a string"],
b: Annotated[str, "Second integer as a string"], b: Annotated[str, "Second integer as a string"],
@ -15,7 +25,16 @@ def gcd(
return str(math.gcd(int(a), int(b))) return str(math.gcd(int(a), int(b)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def lcm( def lcm(
a: Annotated[str, "First integer as a string"], a: Annotated[str, "First integer as a string"],
b: Annotated[str, "Second integer as a string"], b: Annotated[str, "Second integer as a string"],

View file

@ -3,12 +3,22 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def ceil( def ceil(
a: Annotated[str, "The number to round up as a string"], a: Annotated[str, "The number to round up as a string"],
) -> Annotated[str, "The smallest integer greater than or equal to the number as a string"]: ) -> Annotated[str, "The smallest integer greater than or equal to the number as a string"]:
@ -19,7 +29,16 @@ def ceil(
return str(math.ceil(Decimal(a))) return str(math.ceil(Decimal(a)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def floor( def floor(
a: Annotated[str, "The number to round down as a string"], a: Annotated[str, "The number to round down as a string"],
) -> Annotated[str, "The largest integer less than or equal to the number as a string"]: ) -> Annotated[str, "The largest integer less than or equal to the number as a string"]:
@ -30,7 +49,16 @@ def floor(
return str(math.floor(Decimal(a))) return str(math.floor(Decimal(a)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def round_num( def round_num(
value: Annotated[str, "The number to round as a string"], value: Annotated[str, "The number to round as a string"],
ndigits: Annotated[str, "The number of digits after the decimal point as a string"], ndigits: Annotated[str, "The number of digits after the decimal point as a string"],

View file

@ -3,12 +3,22 @@ from decimal import Decimal
from statistics import median as stats_median from statistics import median as stats_median
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def avg( def avg(
numbers: Annotated[list[str], "The list of numbers as strings"], numbers: Annotated[list[str], "The list of numbers as strings"],
) -> Annotated[str, "The average (mean) of the numbers in the list as a string"]: ) -> Annotated[str, "The average (mean) of the numbers in the list as a string"]:
@ -21,7 +31,16 @@ def avg(
return str(sum(d_numbers) / len(d_numbers)) if d_numbers else "0.0" return str(sum(d_numbers) / len(d_numbers)) if d_numbers else "0.0"
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def median( def median(
numbers: Annotated[list[str], "A list of numbers as strings"], numbers: Annotated[list[str], "A list of numbers as strings"],
) -> Annotated[str, "The median value of the numbers in the list as a string"]: ) -> Annotated[str, "The median value of the numbers in the list as a string"]:

View file

@ -3,12 +3,22 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def deg_to_rad( def deg_to_rad(
degrees: Annotated[str, "Angle in degrees as a string"], degrees: Annotated[str, "Angle in degrees as a string"],
) -> Annotated[str, "Angle in radians as a string"]: ) -> Annotated[str, "Angle in radians as a string"]:
@ -19,7 +29,16 @@ def deg_to_rad(
return str(math.radians(Decimal(degrees))) return str(math.radians(Decimal(degrees)))
@tool @tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def rad_to_deg( def rad_to_deg(
radians: Annotated[str, "Angle in radians as a string"], radians: Annotated[str, "Angle in radians as a string"],
) -> Annotated[str, "Angle in degrees as a string"]: ) -> Annotated[str, "Angle in degrees as a string"]:

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_math" name = "arcade_math"
version = "1.0.5" version = "1.1.0"
description = "Arcade.dev LLM tools for doing math" description = "Arcade.dev LLM tools for doing math"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
] ]
[[project.authors]] [[project.authors]]
name = "Arcade" name = "Arcade"
@ -33,6 +34,7 @@ dev = [
arcade-mcp = {path = "../../", editable = true} arcade-mcp = {path = "../../", editable = true}
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]
files = [ "arcade_math/**/*.py",] files = [ "arcade_math/**/*.py",]

View file

@ -1,6 +1,7 @@
import json import json
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError from arcade_tdk.errors import RetryableToolError
@ -20,7 +21,18 @@ from .utils import (
# BANNED = "banned" # BANNED = "banned"
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_databases( async def discover_databases(
context: ToolContext, context: ToolContext,
) -> list[str]: ) -> list[str]:
@ -32,7 +44,18 @@ async def discover_databases(
return databases return databases
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_collections( async def discover_collections(
context: ToolContext, context: ToolContext,
database_name: Annotated[str, "The database name to discover collections in"], database_name: Annotated[str, "The database name to discover collections in"],
@ -48,7 +71,18 @@ async def discover_collections(
return list(collections) return list(collections)
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def get_collection_schema( async def get_collection_schema(
context: ToolContext, context: ToolContext,
database_name: Annotated[str, "The database name to get the collection schema of"], database_name: Annotated[str, "The database name to get the collection schema of"],
@ -90,7 +124,18 @@ async def get_collection_schema(
} }
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def find_documents( async def find_documents(
context: ToolContext, context: ToolContext,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],
@ -193,7 +238,18 @@ async def find_documents(
) from e ) from e
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def count_documents( async def count_documents(
context: ToolContext, context: ToolContext,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],
@ -230,7 +286,18 @@ async def count_documents(
) from e ) from e
@tool(requires_secrets=["MONGODB_CONNECTION_STRING"]) @tool(
requires_secrets=["MONGODB_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def aggregate_documents( async def aggregate_documents(
context: ToolContext, context: ToolContext,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_mongodb" name = "arcade_mongodb"
version = "0.1.1" version = "0.2.0"
description = "Tools to query and explore a MongoDB database" description = "Tools to query and explore a MongoDB database"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"pymongo>=4.10.1", "pymongo>=4.10.1",
"pydantic>=2.11.7", "pydantic>=2.11.7",
"motor>=3.6.0", "motor>=3.6.0",
@ -37,6 +38,7 @@ dev = [
arcade-mcp = { path = "../../", editable = true } arcade-mcp = { path = "../../", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]

View file

@ -1,5 +1,6 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError from arcade_tdk.errors import RetryableToolError
from sqlalchemy import inspect, text from sqlalchemy import inspect, text
@ -8,7 +9,18 @@ from sqlalchemy.ext.asyncio import AsyncEngine
from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
@tool(requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_schemas( async def discover_schemas(
context: ToolContext, context: ToolContext,
) -> list[str]: ) -> list[str]:
@ -20,7 +32,18 @@ async def discover_schemas(
return schemas return schemas
@tool(requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def discover_tables( async def discover_tables(
context: ToolContext, context: ToolContext,
schema_name: Annotated[ schema_name: Annotated[
@ -38,7 +61,18 @@ async def discover_tables(
return tables return tables
@tool(requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def get_table_schema( async def get_table_schema(
context: ToolContext, context: ToolContext,
schema_name: Annotated[str, "The database schema to get the table schema of"], schema_name: Annotated[str, "The database schema to get the table schema of"],
@ -55,7 +89,18 @@ async def get_table_schema(
return await _get_table_schema(engine, schema_name, table_name) return await _get_table_schema(engine, schema_name, table_name)
@tool(requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"]) @tool(
requires_secrets=["POSTGRES_DATABASE_CONNECTION_STRING"],
metadata=ToolMetadata(
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
)
async def execute_select_query( async def execute_select_query(
context: ToolContext, context: ToolContext,
select_clause: Annotated[ select_clause: Annotated[

View file

@ -4,11 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_postgres" name = "arcade_postgres"
version = "0.3.1" version = "0.4.0"
description = "Tools to query and explore a postgres database" description = "Tools to query and explore a postgres database"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"psycopg2-binary>=2.9.10", "psycopg2-binary>=2.9.10",
"pydantic>=2.11.7", "pydantic>=2.11.7",
"sqlalchemy>=2.0.41", "sqlalchemy>=2.0.41",
@ -40,6 +41,7 @@ dev = [
arcade-mcp = { path = "../../", editable = true } arcade-mcp = { path = "../../", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]

View file

@ -7,9 +7,9 @@ from arcade_zendesk.tools import (
) )
__all__ = [ __all__ = [
"list_tickets",
"add_ticket_comment", "add_ticket_comment",
"get_ticket_comments", "get_ticket_comments",
"list_tickets",
"mark_ticket_solved", "mark_ticket_solved",
"search_articles", "search_articles",
] ]

View file

@ -8,9 +8,9 @@ from arcade_zendesk.tools.tickets import (
) )
__all__ = [ __all__ = [
"list_tickets",
"add_ticket_comment", "add_ticket_comment",
"get_ticket_comments", "get_ticket_comments",
"list_tickets",
"mark_ticket_solved", "mark_ticket_solved",
"search_articles", "search_articles",
"who_am_i", "who_am_i",

View file

@ -2,6 +2,13 @@ import logging
from typing import Annotated, Any from typing import Annotated, Any
import httpx import httpx
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2 from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError from arcade_tdk.errors import RetryableToolError
@ -20,6 +27,18 @@ logger = logging.getLogger(__name__)
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]), requires_auth=OAuth2(id="zendesk", scopes=["read"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
) )
async def search_articles( async def search_articles(
context: ToolContext, context: ToolContext,

View file

@ -1,5 +1,12 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2 from arcade_tdk.auth import OAuth2
@ -9,6 +16,18 @@ from arcade_zendesk.who_am_i_util import build_who_am_i_response
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]), requires_auth=OAuth2(id="zendesk", scopes=["read"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
) )
async def who_am_i( async def who_am_i(
context: ToolContext, context: ToolContext,

View file

@ -1,6 +1,13 @@
from typing import Annotated, Any from typing import Annotated, Any
import httpx import httpx
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2 from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError from arcade_tdk.errors import RetryableToolError
@ -23,6 +30,18 @@ def _handle_ticket_not_found(response: httpx.Response, ticket_id: int) -> None:
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]), requires_auth=OAuth2(id="zendesk", scopes=["read"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
) )
async def list_tickets( async def list_tickets(
context: ToolContext, context: ToolContext,
@ -133,6 +152,18 @@ async def list_tickets(
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]), requires_auth=OAuth2(id="zendesk", scopes=["read"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.READ],
read_only=True,
destructive=False,
idempotent=True,
open_world=True,
),
),
) )
async def get_ticket_comments( async def get_ticket_comments(
context: ToolContext, context: ToolContext,
@ -184,6 +215,18 @@ async def get_ticket_comments(
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]), requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.CREATE],
read_only=False,
destructive=False,
idempotent=False,
open_world=True,
),
),
) )
async def add_ticket_comment( async def add_ticket_comment(
context: ToolContext, context: ToolContext,
@ -243,6 +286,18 @@ async def add_ticket_comment(
@tool( @tool(
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]), requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
requires_secrets=["ZENDESK_SUBDOMAIN"], requires_secrets=["ZENDESK_SUBDOMAIN"],
metadata=ToolMetadata(
classification=Classification(
service_domains=[ServiceDomain.CUSTOMER_SUPPORT],
),
behavior=Behavior(
operations=[Operation.UPDATE, Operation.CREATE],
read_only=False,
destructive=False,
idempotent=False,
open_world=True,
),
),
) )
async def mark_ticket_solved( async def mark_ticket_solved(
context: ToolContext, context: ToolContext,

View file

@ -4,10 +4,11 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_zendesk" name = "arcade_zendesk"
version = "0.3.1" version = "0.4.0"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0", "arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.0.0",
"httpx>=0.25.0,<1.0.0", "httpx>=0.25.0,<1.0.0",
"beautifulsoup4>=4.0.0,<5" "beautifulsoup4>=4.0.0,<5"
] ]
@ -32,6 +33,7 @@ dev = [
arcade-mcp = { path = "../../", editable = true } arcade-mcp = { path = "../../", editable = true }
arcade-serve = { path = "../../libs/arcade-serve/", editable = true } arcade-serve = { path = "../../libs/arcade-serve/", editable = true }
arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true } arcade-tdk = { path = "../../libs/arcade-tdk/", editable = true }
arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]