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
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_brightdata.bright_data_client import BrightDataClient
@ -51,7 +58,21 @@ class SourceType(str, Enum):
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(
context: ToolContext,
url: Annotated[str, "URL to scrape"],
@ -71,7 +92,21 @@ def scrape_as_markdown(
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
context: ToolContext,
query: Annotated[str, "Search query"],
@ -167,7 +202,21 @@ def search_engine( # noqa: C901
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(
context: ToolContext,
source_type: Annotated[SourceType, "Type of data source"],

View file

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

View file

@ -1,12 +1,24 @@
from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError
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(
context: ToolContext,
) -> list[str]:
@ -18,7 +30,18 @@ async def discover_schemas(
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(
context: ToolContext,
) -> list[str]:
@ -30,7 +53,18 @@ async def discover_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(
context: ToolContext,
) -> list[str]:
@ -45,7 +79,18 @@ async def discover_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(
context: ToolContext,
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)
@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(
context: ToolContext,
select_clause: Annotated[

View file

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

View file

@ -1,5 +1,12 @@
from typing import Annotated
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import LinkedIn
from arcade_tdk.errors import ToolExecutionError
@ -10,7 +17,19 @@ from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linked
@tool(
requires_auth=LinkedIn(
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(
context: ToolContext,

View file

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

View file

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

View file

@ -2,12 +2,22 @@ import decimal
from decimal import Decimal
from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def add(
a: Annotated[str, "The first 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)
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def subtract(
a: Annotated[str, "The first 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)
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def multiply(
a: Annotated[str, "The first 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)
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def divide(
a: Annotated[str, "The first 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)
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def sum_list(
numbers: Annotated[list[str], "The list of numbers as strings"],
) -> 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]))
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def sum_range(
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"],
@ -85,7 +140,16 @@ def sum_range(
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(
a: Annotated[str, "The dividend as a string"],
b: Annotated[str, "The divisor as a string"],

View file

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

View file

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

View file

@ -1,10 +1,20 @@
import random
from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
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(
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"],
@ -21,7 +31,16 @@ def generate_random_int(
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(
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"],

View file

@ -1,10 +1,20 @@
import math
from typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def gcd(
a: Annotated[str, "First 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)))
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def lcm(
a: Annotated[str, "First 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 typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def ceil(
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"]:
@ -19,7 +29,16 @@ def ceil(
return str(math.ceil(Decimal(a)))
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def floor(
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"]:
@ -30,7 +49,16 @@ def floor(
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(
value: Annotated[str, "The number to round 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 typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def avg(
numbers: Annotated[list[str], "The list of numbers as strings"],
) -> 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"
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def median(
numbers: Annotated[list[str], "A list of numbers as strings"],
) -> 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 typing import Annotated
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def deg_to_rad(
degrees: Annotated[str, "Angle in degrees 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)))
@tool
@tool(
metadata=ToolMetadata(
behavior=Behavior(
read_only=True,
destructive=False,
idempotent=True,
open_world=False,
),
),
)
def rad_to_deg(
radians: Annotated[str, "Angle in radians as a string"],
) -> Annotated[str, "Angle in degrees as a string"]:

View file

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

View file

@ -1,6 +1,7 @@
import json
from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError
@ -20,7 +21,18 @@ from .utils import (
# 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(
context: ToolContext,
) -> list[str]:
@ -32,7 +44,18 @@ async def discover_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(
context: ToolContext,
database_name: Annotated[str, "The database name to discover collections in"],
@ -48,7 +71,18 @@ async def discover_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(
context: ToolContext,
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(
context: ToolContext,
database_name: Annotated[str, "The database name to query"],
@ -193,7 +238,18 @@ async def find_documents(
) 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(
context: ToolContext,
database_name: Annotated[str, "The database name to query"],
@ -230,7 +286,18 @@ async def count_documents(
) 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(
context: ToolContext,
database_name: Annotated[str, "The database name to query"],

View file

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

View file

@ -1,5 +1,6 @@
from typing import Annotated, Any
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata
from arcade_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError
from sqlalchemy import inspect, text
@ -8,7 +9,18 @@ from sqlalchemy.ext.asyncio import AsyncEngine
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(
context: ToolContext,
) -> list[str]:
@ -20,7 +32,18 @@ async def discover_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(
context: ToolContext,
schema_name: Annotated[
@ -38,7 +61,18 @@ async def discover_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(
context: ToolContext,
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)
@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(
context: ToolContext,
select_clause: Annotated[

View file

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

View file

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

View file

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

View file

@ -2,6 +2,13 @@ import logging
from typing import Annotated, Any
import httpx
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError
@ -20,6 +27,18 @@ logger = logging.getLogger(__name__)
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]),
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(
context: ToolContext,

View file

@ -1,5 +1,12 @@
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.auth import OAuth2
@ -9,6 +16,18 @@ from arcade_zendesk.who_am_i_util import build_who_am_i_response
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]),
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(
context: ToolContext,

View file

@ -1,6 +1,13 @@
from typing import Annotated, Any
import httpx
from arcade_mcp_server.metadata import (
Behavior,
Classification,
Operation,
ServiceDomain,
ToolMetadata,
)
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError
@ -23,6 +30,18 @@ def _handle_ticket_not_found(response: httpx.Response, ticket_id: int) -> None:
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]),
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(
context: ToolContext,
@ -133,6 +152,18 @@ async def list_tickets(
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["read"]),
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(
context: ToolContext,
@ -184,6 +215,18 @@ async def get_ticket_comments(
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
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(
context: ToolContext,
@ -243,6 +286,18 @@ async def add_ticket_comment(
@tool(
requires_auth=OAuth2(id="zendesk", scopes=["tickets:write"]),
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(
context: ToolContext,

View file

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