Migrate OSS toolkits to MCPApp (#782)

<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Medium Risk**
> Touches multiple toolkits’ runtime entrypoints and context/error/auth
plumbing, so breakage risk is mainly around invocation/packaging and
tool execution wiring rather than business logic.
> 
> **Overview**
> Migrates the BrightData, ClickHouse, LinkedIn, Math, MongoDB,
Postgres, and Zendesk OSS toolkits from `arcade-tdk` to
`arcade-mcp-server` APIs by updating tool decorators, `Context` types,
auth classes, and exception imports.
> 
> Adds per-toolkit `__main__.py` files that construct an `MCPApp`,
register module tools, and run via configurable transport/host/port;
corresponding `pyproject.toml` updates bump versions, drop
`arcade-tdk`/`arcade-serve` deps, and add `project.scripts` console
entrypoints.
> 
> Updates tests and eval suites to use `arcade_mcp_server.Context`
(mocked) and switches eval `ToolCatalog` imports to `arcade_core`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9b3e31acb4b35e1d72efd47e2d279c5b19e3ecb0. 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 14:29:18 -08:00 committed by GitHub
parent 36584942f7
commit c50699d5e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 361 additions and 176 deletions

View file

@ -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 arcade_brightdata
app = MCPApp(
name="BrightData",
instructions=(
"Use this server when you need to interact with Bright Data to help users "
"scrape web pages, search the web, and extract structured data from websites."
),
)
app.add_tools_from_module(arcade_brightdata)
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()

View file

@ -4,7 +4,8 @@ from enum import Enum
from typing import Annotated, Any, cast from typing import Annotated, Any, cast
import requests import requests
from arcade_core.errors import RetryableToolError from arcade_mcp_server import Context, tool
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import ( from arcade_mcp_server.metadata import (
Behavior, Behavior,
Classification, Classification,
@ -12,7 +13,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain, ServiceDomain,
ToolMetadata, ToolMetadata,
) )
from arcade_tdk import ToolContext, tool
from arcade_brightdata.bright_data_client import BrightDataClient from arcade_brightdata.bright_data_client import BrightDataClient
@ -74,7 +74,7 @@ class SourceType(str, Enum):
), ),
) )
def scrape_as_markdown( def scrape_as_markdown(
context: ToolContext, context: Context,
url: Annotated[str, "URL to scrape"], url: Annotated[str, "URL to scrape"],
) -> Annotated[str, "Scraped webpage content as Markdown"]: ) -> Annotated[str, "Scraped webpage content as Markdown"]:
""" """
@ -108,7 +108,7 @@ def scrape_as_markdown(
), ),
) )
def search_engine( # noqa: C901 def search_engine( # noqa: C901
context: ToolContext, context: Context,
query: Annotated[str, "Search query"], query: Annotated[str, "Search query"],
engine: Annotated[SearchEngine, "Search engine to use"] = SearchEngine.GOOGLE, engine: Annotated[SearchEngine, "Search engine to use"] = SearchEngine.GOOGLE,
language: Annotated[str | None, "Two-letter language code"] = None, language: Annotated[str | None, "Two-letter language code"] = None,
@ -218,7 +218,7 @@ def search_engine( # noqa: C901
), ),
) )
def web_data_feed( def web_data_feed(
context: ToolContext, context: Context,
source_type: Annotated[SourceType, "Type of data source"], source_type: Annotated[SourceType, "Type of data source"],
url: Annotated[str, "URL of the web resource to extract data from"], url: Annotated[str, "URL of the web resource to extract data from"],
num_of_reviews: Annotated[ num_of_reviews: Annotated[

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_brightdata" name = "arcade_brightdata"
version = "0.3.0" version = "0.4.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-mcp-server>=1.17.0,<2.0.0", "arcade-mcp-server>=1.17.0,<2.0.0",
"requests>=2.32.5", "requests>=2.32.5",
] ]
@ -16,10 +15,13 @@ dependencies = [
name = "meirk-brd" name = "meirk-brd"
email = "meirk@brightdata.com" email = "meirk@brightdata.com"
[project.scripts]
arcade-brightdata = "arcade_brightdata.__main__:main"
arcade_brightdata = "arcade_brightdata.__main__:main"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0", "pytest-mock>=3.11.1,<3.12.0",
@ -48,8 +50,6 @@ ignore_missing_imports = "True"
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = { path = "../../", editable = 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.pytest.ini_options] [tool.pytest.ini_options]

View file

@ -1,10 +1,11 @@
from os import environ from os import environ
from unittest.mock import MagicMock as _MagicMock
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
import requests import requests
from arcade_tdk import ToolContext, ToolSecretItem from arcade_mcp_server import Context
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_brightdata.bright_data_client import BrightDataClient from arcade_brightdata.bright_data_client import BrightDataClient
from arcade_brightdata.tools.bright_data_tools import ( from arcade_brightdata.tools.bright_data_tools import (
@ -21,10 +22,13 @@ BRIGHTDATA_ZONE = environ.get("TEST_BRIGHTDATA_ZONE") or "unblocker"
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = _MagicMock(spec=Context)
context.secrets = [] context.get_secret = _MagicMock(
context.secrets.append(ToolSecretItem(key="BRIGHTDATA_API_KEY", value=BRIGHTDATA_API_KEY)) side_effect=lambda key: {
context.secrets.append(ToolSecretItem(key="BRIGHTDATA_ZONE", value=BRIGHTDATA_ZONE)) "BRIGHTDATA_API_KEY": BRIGHTDATA_API_KEY,
"BRIGHTDATA_ZONE": BRIGHTDATA_ZONE,
}[key]
)
return context return context

View file

@ -0,0 +1,29 @@
import sys
from typing import cast
from arcade_mcp_server import MCPApp
from arcade_mcp_server.mcp_app import TransportType
import arcade_clickhouse
app = MCPApp(
name="ClickHouse",
instructions=(
"Use this server when you need to interact with ClickHouse to help users "
"query, explore, and manage their ClickHouse databases."
),
)
app.add_tools_from_module(arcade_clickhouse)
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()

View file

@ -3,7 +3,7 @@ from typing import Any, ClassVar
from urllib.parse import urlparse from urllib.parse import urlparse
import clickhouse_connect import clickhouse_connect
from arcade_tdk.errors import RetryableToolError from arcade_mcp_server.exceptions import RetryableToolError
MAX_ROWS_RETURNED = 1000 MAX_ROWS_RETURNED = 1000
TEST_QUERY = "SELECT 1" TEST_QUERY = "SELECT 1"

View file

@ -1,8 +1,8 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server import Context, tool
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata 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 from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
@ -20,7 +20,7 @@ from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
), ),
) )
async def discover_schemas( async def discover_schemas(
context: ToolContext, context: Context,
) -> list[str]: ) -> list[str]:
"""Discover all the schemas in the ClickHouse database. """Discover all the schemas in the ClickHouse database.
@ -43,7 +43,7 @@ async def discover_schemas(
), ),
) )
async def discover_databases( async def discover_databases(
context: ToolContext, context: Context,
) -> list[str]: ) -> list[str]:
"""Discover all the databases in the ClickHouse database.""" """Discover all the databases in the ClickHouse database."""
async with await DatabaseEngine.get_engine( async with await DatabaseEngine.get_engine(
@ -66,7 +66,7 @@ async def discover_databases(
), ),
) )
async def discover_tables( async def discover_tables(
context: ToolContext, context: Context,
) -> list[str]: ) -> list[str]:
"""Discover all the tables in the ClickHouse database when the list of tables is not known. """Discover all the tables in the ClickHouse database when the list of tables is not known.
@ -92,7 +92,7 @@ async def discover_tables(
), ),
) )
async def get_table_schema( async def get_table_schema(
context: ToolContext, context: Context,
schema_name: Annotated[str, "The schema to get the table schema of"], schema_name: Annotated[str, "The schema to get the table schema of"],
table_name: Annotated[str, "The table to get the schema of"], table_name: Annotated[str, "The table to get the schema of"],
) -> list[str]: ) -> list[str]:
@ -120,7 +120,7 @@ async def get_table_schema(
), ),
) )
async def execute_select_query( async def execute_select_query(
context: ToolContext, context: Context,
select_clause: Annotated[ select_clause: Annotated[
str, str,
"This is the part of the SQL query that comes after the SELECT keyword wish a comma separated list of columns you wish to return. Do not include the SELECT keyword.", "This is the part of the SQL query that comes after the SELECT keyword wish a comma separated list of columns you wish to return. Do not include the SELECT keyword.",

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_clickhouse" name = "arcade_clickhouse"
version = "0.2.0" version = "0.3.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-mcp-server>=1.17.0,<2.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",
@ -22,11 +21,9 @@ dependencies = [
name = "evantahler" name = "evantahler"
email = "support@arcade.dev" email = "support@arcade.dev"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0", "pytest-mock>=3.11.1,<3.12.0",
@ -37,14 +34,15 @@ dev = [
"ruff>=0.7.4,<0.8.0", "ruff>=0.7.4,<0.8.0",
] ]
[project.scripts]
arcade-clickhouse = "arcade_clickhouse.__main__:main"
arcade_clickhouse = "arcade_clickhouse.__main__:main"
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = { path = "../../", editable = 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]
files = [ "arcade_clickhouse/**/*.py",] files = [ "arcade_clickhouse/**/*.py",]
python_version = "3.10" python_version = "3.10"

View file

@ -1,5 +1,6 @@
import os import os
from os import environ from os import environ
from unittest.mock import MagicMock
import pytest import pytest
import pytest_asyncio import pytest_asyncio
@ -10,8 +11,8 @@ from arcade_clickhouse.tools.clickhouse import (
execute_select_query, execute_select_query,
get_table_schema, get_table_schema,
) )
from arcade_tdk import ToolContext, ToolSecretItem from arcade_mcp_server import Context
from arcade_tdk.errors import RetryableToolError from arcade_mcp_server.exceptions import RetryableToolError
CLICKHOUSE_DATABASE_CONNECTION_STRING = ( CLICKHOUSE_DATABASE_CONNECTION_STRING = (
environ.get("TEST_CLICKHOUSE_DATABASE_CONNECTION_STRING") environ.get("TEST_CLICKHOUSE_DATABASE_CONNECTION_STRING")
@ -21,14 +22,8 @@ CLICKHOUSE_DATABASE_CONNECTION_STRING = (
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = MagicMock(spec=Context)
context.secrets = [] context.get_secret = MagicMock(return_value=CLICKHOUSE_DATABASE_CONNECTION_STRING)
context.secrets.append(
ToolSecretItem(
key="CLICKHOUSE_DATABASE_CONNECTION_STRING", value=CLICKHOUSE_DATABASE_CONNECTION_STRING
)
)
return context return context

View file

@ -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 arcade_linkedin
app = MCPApp(
name="LinkedIn",
instructions=(
"Use this server when you need to interact with LinkedIn to help users "
"create and share posts on their LinkedIn profile."
),
)
app.add_tools_from_module(arcade_linkedin)
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()

View file

@ -1,5 +1,8 @@
from typing import Annotated from typing import Annotated
from arcade_mcp_server import Context, tool
from arcade_mcp_server.auth import LinkedIn
from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_mcp_server.metadata import ( from arcade_mcp_server.metadata import (
Behavior, Behavior,
Classification, Classification,
@ -7,9 +10,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain, ServiceDomain,
ToolMetadata, ToolMetadata,
) )
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import LinkedIn
from arcade_tdk.errors import ToolExecutionError
from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linkedin_request from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linkedin_request
@ -32,7 +32,7 @@ from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linked
), ),
) )
async def create_text_post( async def create_text_post(
context: ToolContext, context: Context,
text: Annotated[str, "The text content of the post"], text: Annotated[str, "The text content of the post"],
) -> Annotated[str, "URL of the shared post"]: ) -> Annotated[str, "URL of the shared post"]:
"""Share a new text post to LinkedIn.""" """Share a new text post to LinkedIn."""

View file

@ -1,12 +1,12 @@
import httpx import httpx
from arcade_tdk import ToolContext from arcade_mcp_server import Context
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_linkedin.tools.constants import LINKEDIN_BASE_URL from arcade_linkedin.tools.constants import LINKEDIN_BASE_URL
async def _send_linkedin_request( async def _send_linkedin_request(
context: ToolContext, context: Context,
method: str, method: str,
endpoint: str, endpoint: str,
params: dict | None = None, params: dict | None = None,

View file

@ -1,14 +1,18 @@
from unittest.mock import MagicMock
import pytest import pytest
from arcade_tdk import ToolAuthorizationContext, ToolContext from arcade_mcp_server import Context
@pytest.fixture @pytest.fixture
def tool_context(): def tool_context():
"""Fixture for the ToolContext with mock authorization.""" """Fixture for the tool Context with mock authorization."""
return ToolContext( context = MagicMock(spec=Context)
authorization=ToolAuthorizationContext(token="test_token", user_info={"sub": "test_user"}), # noqa: S106 authorization = MagicMock()
user_id="test_user", authorization.token = "test_token" # noqa: S105
) authorization.user_info = {"sub": "test_user"}
context.authorization = authorization
return context
@pytest.fixture @pytest.fixture

View file

@ -1,3 +1,4 @@
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
EvalRubric, EvalRubric,
EvalSuite, EvalSuite,
@ -5,7 +6,6 @@ from arcade_evals import (
SimilarityCritic, SimilarityCritic,
tool_eval, tool_eval,
) )
from arcade_tdk import ToolCatalog
import arcade_linkedin import arcade_linkedin
from arcade_linkedin.tools.share import create_text_post from arcade_linkedin.tools.share import create_text_post

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_linkedin" name = "arcade_linkedin"
version = "0.2.0" version = "0.3.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-mcp-server>=1.17.0,<2.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",
] ]
@ -16,10 +15,13 @@ dependencies = [
name = "Arcade" name = "Arcade"
email = "dev@arcade.dev" email = "dev@arcade.dev"
[project.scripts]
arcade-linkedin = "arcade_linkedin.__main__:main"
arcade_linkedin = "arcade_linkedin.__main__:main"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-asyncio>=0.24.0,<0.25.0", "pytest-asyncio>=0.24.0,<0.25.0",
@ -33,8 +35,6 @@ dev = [
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = {path = "../../", editable = true} 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]

View file

@ -1,7 +1,7 @@
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_linkedin.tools.share import create_text_post from arcade_linkedin.tools.share import create_text_post

View file

@ -0,0 +1,29 @@
import sys
from typing import cast
from arcade_mcp_server import MCPApp
from arcade_mcp_server.mcp_app import TransportType
import arcade_math
app = MCPApp(
name="Math",
instructions=(
"Use this server when you need to perform mathematical calculations to help users "
"with arithmetic, trigonometry, statistics, exponents, rounding, and other math operations."
),
)
app.add_tools_from_module(arcade_math)
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()

View file

@ -2,8 +2,8 @@ import decimal
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -3,8 +3,8 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -3,8 +3,8 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -1,8 +1,8 @@
import random import random
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
@tool( @tool(

View file

@ -1,8 +1,8 @@
import math import math
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
@tool( @tool(

View file

@ -3,8 +3,8 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -3,8 +3,8 @@ 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 import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -3,8 +3,8 @@ import math
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100 decimal.getcontext().prec = 100

View file

@ -1,6 +1,7 @@
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
BinaryCritic, BinaryCritic,
EvalRubric, EvalRubric,
@ -8,7 +9,6 @@ from arcade_evals import (
ExpectedToolCall, ExpectedToolCall,
tool_eval, tool_eval,
) )
from arcade_tdk import ToolCatalog
import arcade_math import arcade_math
from arcade_math.tools.arithmetic import ( from arcade_math.tools.arithmetic import (

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_math" name = "arcade_math"
version = "1.1.0" version = "1.2.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-mcp-server>=1.17.0,<2.0.0", "arcade-mcp-server>=1.17.0,<2.0.0",
] ]
[[project.authors]] [[project.authors]]
@ -18,7 +17,6 @@ email = "dev@arcade.dev"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-asyncio>=0.24.0,<0.25.0", "pytest-asyncio>=0.24.0,<0.25.0",
@ -29,11 +27,13 @@ dev = [
"ruff>=0.7.4,<0.8.0", "ruff>=0.7.4,<0.8.0",
] ]
[project.scripts]
arcade-math = "arcade_math.__main__:main"
arcade_math = "arcade_math.__main__:main"
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = {path = "../../", editable = true} 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]

View file

@ -1,5 +1,5 @@
import pytest import pytest
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_math.tools.arithmetic import ( from arcade_math.tools.arithmetic import (
add, add,

View file

@ -1,5 +1,5 @@
import pytest import pytest
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_math.tools.exponents import ( from arcade_math.tools.exponents import (
log, log,

View file

@ -1,5 +1,5 @@
import pytest import pytest
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_math.tools.miscellaneous import ( from arcade_math.tools.miscellaneous import (
abs_val, abs_val,

View file

@ -1,5 +1,5 @@
import pytest import pytest
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_math.tools.rational import ( from arcade_math.tools.rational import (
gcd, gcd,

View file

@ -0,0 +1,29 @@
import sys
from typing import cast
from arcade_mcp_server import MCPApp
from arcade_mcp_server.mcp_app import TransportType
import arcade_mongodb
app = MCPApp(
name="MongoDB",
instructions=(
"Use this server when you need to interact with MongoDB to help users "
"query, explore, and manage their MongoDB databases and collections."
),
)
app.add_tools_from_module(arcade_mongodb)
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()

View file

@ -1,6 +1,6 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from arcade_tdk.errors import RetryableToolError from arcade_mcp_server.exceptions import RetryableToolError
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
from pymongo.errors import ServerSelectionTimeoutError from pymongo.errors import ServerSelectionTimeoutError

View file

@ -1,9 +1,9 @@
import json import json
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server import Context, tool
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata 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_RECORDS_RETURNED, DatabaseEngine from ..database_engine import MAX_RECORDS_RETURNED, DatabaseEngine
from .utils import ( from .utils import (
@ -34,7 +34,7 @@ from .utils import (
), ),
) )
async def discover_databases( async def discover_databases(
context: ToolContext, context: Context,
) -> list[str]: ) -> list[str]:
"""Discover all the databases in the MongoDB instance.""" """Discover all the databases in the MongoDB instance."""
client = await DatabaseEngine.get_instance(context.get_secret("MONGODB_CONNECTION_STRING")) client = await DatabaseEngine.get_instance(context.get_secret("MONGODB_CONNECTION_STRING"))
@ -57,7 +57,7 @@ async def discover_databases(
), ),
) )
async def discover_collections( async def discover_collections(
context: ToolContext, context: Context,
database_name: Annotated[str, "The database name to discover collections in"], database_name: Annotated[str, "The database name to discover collections in"],
) -> list[str]: ) -> list[str]:
"""Discover all the collections in the MongoDB database when the list of collections is not known. """Discover all the collections in the MongoDB database when the list of collections is not known.
@ -84,7 +84,7 @@ async def discover_collections(
), ),
) )
async def get_collection_schema( async def get_collection_schema(
context: ToolContext, context: Context,
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"],
collection_name: Annotated[str, "The collection to get the schema of"], collection_name: Annotated[str, "The collection to get the schema of"],
sample_size: Annotated[ sample_size: Annotated[
@ -137,7 +137,7 @@ async def get_collection_schema(
), ),
) )
async def find_documents( async def find_documents(
context: ToolContext, context: Context,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],
collection_name: Annotated[str, "The collection name to query"], collection_name: Annotated[str, "The collection name to query"],
filter_dict: Annotated[ filter_dict: Annotated[
@ -251,7 +251,7 @@ async def find_documents(
), ),
) )
async def count_documents( async def count_documents(
context: ToolContext, context: Context,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],
collection_name: Annotated[str, "The collection name to query"], collection_name: Annotated[str, "The collection name to query"],
filter_dict: Annotated[ filter_dict: Annotated[
@ -299,7 +299,7 @@ async def count_documents(
), ),
) )
async def aggregate_documents( async def aggregate_documents(
context: ToolContext, context: Context,
database_name: Annotated[str, "The database name to query"], database_name: Annotated[str, "The database name to query"],
collection_name: Annotated[str, "The collection name to query"], collection_name: Annotated[str, "The collection name to query"],
pipeline: Annotated[ pipeline: Annotated[

View file

@ -2,7 +2,7 @@ import json
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from arcade_tdk.errors import RetryableToolError from arcade_mcp_server.exceptions import RetryableToolError
from bson import ObjectId from bson import ObjectId

View file

@ -1,6 +1,7 @@
# RUN ME WITH `uv run arcade evals evals --host api.arcade.dev` # RUN ME WITH `uv run arcade evals evals --host api.arcade.dev`
import arcade_mongodb import arcade_mongodb
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
BinaryCritic, BinaryCritic,
EvalRubric, EvalRubric,
@ -17,7 +18,6 @@ from arcade_mongodb.tools.mongodb import (
find_documents, find_documents,
get_collection_schema, get_collection_schema,
) )
from arcade_tdk import ToolCatalog
# Evaluation rubric # Evaluation rubric
rubric = EvalRubric( rubric = EvalRubric(

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_mongodb" name = "arcade_mongodb"
version = "0.2.0" version = "0.3.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-mcp-server>=1.17.0,<2.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",
@ -18,11 +17,9 @@ dependencies = [
name = "evantahler" name = "evantahler"
email = "support@arcade.dev" email = "support@arcade.dev"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0", "pytest-mock>=3.11.1,<3.12.0",
@ -33,14 +30,15 @@ dev = [
"ruff>=0.7.4,<0.8.0", "ruff>=0.7.4,<0.8.0",
] ]
[project.scripts]
arcade-mongodb = "arcade_mongodb.__main__:main"
arcade_mongodb = "arcade_mongodb.__main__:main"
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = { path = "../../", editable = 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]
files = [ "arcade_mongodb/**/*.py",] files = [ "arcade_mongodb/**/*.py",]
python_version = "3.10" python_version = "3.10"

View file

@ -1,19 +1,18 @@
from unittest.mock import MagicMock
import pytest import pytest
from arcade_core.errors import ToolExecutionError from arcade_core.errors import ToolExecutionError
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mongodb.tools.mongodb import aggregate_documents, count_documents, find_documents from arcade_mongodb.tools.mongodb import aggregate_documents, count_documents, find_documents
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from .conftest import TEST_MONGODB_CONNECTION_STRING from .conftest import TEST_MONGODB_CONNECTION_STRING
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = MagicMock(spec=Context)
context.secrets = [] context.get_secret = MagicMock(return_value=TEST_MONGODB_CONNECTION_STRING)
context.secrets.append(
ToolSecretItem(key="MONGODB_CONNECTION_STRING", value=TEST_MONGODB_CONNECTION_STRING)
)
return context return context

View file

@ -1,6 +1,9 @@
import json import json
from unittest.mock import MagicMock
import pytest import pytest
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mongodb.database_engine import DatabaseEngine from arcade_mongodb.database_engine import DatabaseEngine
from arcade_mongodb.tools.mongodb import ( from arcade_mongodb.tools.mongodb import (
# UserStatus, # UserStatus,
@ -12,19 +15,14 @@ from arcade_mongodb.tools.mongodb import (
get_collection_schema, get_collection_schema,
# update_user_status, # update_user_status,
) )
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from .conftest import TEST_MONGODB_CONNECTION_STRING from .conftest import TEST_MONGODB_CONNECTION_STRING
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = MagicMock(spec=Context)
context.secrets = [] context.get_secret = MagicMock(return_value=TEST_MONGODB_CONNECTION_STRING)
context.secrets.append(
ToolSecretItem(key="MONGODB_CONNECTION_STRING", value=TEST_MONGODB_CONNECTION_STRING)
)
return context return context

View file

@ -1,18 +1,17 @@
from unittest.mock import MagicMock
import pytest import pytest
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mongodb.tools.mongodb import aggregate_documents, count_documents, find_documents from arcade_mongodb.tools.mongodb import aggregate_documents, count_documents, find_documents
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from .conftest import TEST_MONGODB_CONNECTION_STRING from .conftest import TEST_MONGODB_CONNECTION_STRING
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = MagicMock(spec=Context)
context.secrets = [] context.get_secret = MagicMock(return_value=TEST_MONGODB_CONNECTION_STRING)
context.secrets.append(
ToolSecretItem(key="MONGODB_CONNECTION_STRING", value=TEST_MONGODB_CONNECTION_STRING)
)
return context return context

View file

@ -0,0 +1,29 @@
import sys
from typing import cast
from arcade_mcp_server import MCPApp
from arcade_mcp_server.mcp_app import TransportType
import arcade_postgres
app = MCPApp(
name="PostgreSQL",
instructions=(
"Use this server when you need to interact with PostgreSQL to help users "
"query, explore, and manage their PostgreSQL databases."
),
)
app.add_tools_from_module(arcade_postgres)
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()

View file

@ -1,7 +1,7 @@
from typing import Any, ClassVar from typing import Any, ClassVar
from urllib.parse import urlparse from urllib.parse import urlparse
from arcade_tdk.errors import RetryableToolError from arcade_mcp_server.exceptions import RetryableToolError
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine

View file

@ -1,8 +1,8 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server import Context, tool
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import Behavior, Operation, ToolMetadata 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 from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import AsyncEngine
@ -22,7 +22,7 @@ from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
), ),
) )
async def discover_schemas( async def discover_schemas(
context: ToolContext, context: Context,
) -> list[str]: ) -> list[str]:
"""Discover all the schemas in the postgres database.""" """Discover all the schemas in the postgres database."""
async with await DatabaseEngine.get_engine( async with await DatabaseEngine.get_engine(
@ -45,7 +45,7 @@ async def discover_schemas(
), ),
) )
async def discover_tables( async def discover_tables(
context: ToolContext, context: Context,
schema_name: Annotated[ schema_name: Annotated[
str, "The database schema to discover tables in (default value: 'public')" str, "The database schema to discover tables in (default value: 'public')"
] = "public", ] = "public",
@ -74,7 +74,7 @@ async def discover_tables(
), ),
) )
async def get_table_schema( async def get_table_schema(
context: ToolContext, context: Context,
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"],
table_name: Annotated[str, "The table to get the schema of"], table_name: Annotated[str, "The table to get the schema of"],
) -> list[str]: ) -> list[str]:
@ -102,7 +102,7 @@ async def get_table_schema(
), ),
) )
async def execute_select_query( async def execute_select_query(
context: ToolContext, context: Context,
select_clause: Annotated[ select_clause: Annotated[
str, str,
"This is the part of the SQL query that comes after the SELECT keyword wish a comma separated list of columns you wish to return. Do not include the SELECT keyword.", "This is the part of the SQL query that comes after the SELECT keyword wish a comma separated list of columns you wish to return. Do not include the SELECT keyword.",

View file

@ -1,4 +1,5 @@
import arcade_postgres import arcade_postgres
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
BinaryCritic, BinaryCritic,
EvalRubric, EvalRubric,
@ -12,7 +13,6 @@ from arcade_postgres.tools.postgres import (
execute_query, execute_query,
get_table_schema, get_table_schema,
) )
from arcade_tdk import ToolCatalog
# Evaluation rubric # Evaluation rubric
rubric = EvalRubric( rubric = EvalRubric(

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_postgres" name = "arcade_postgres"
version = "0.4.0" version = "0.5.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-mcp-server>=1.17.0,<2.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",
@ -21,11 +20,9 @@ dependencies = [
name = "evantahler" name = "evantahler"
email = "support@arcade.dev" email = "support@arcade.dev"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0", "pytest-mock>=3.11.1,<3.12.0",
@ -36,14 +33,15 @@ dev = [
"ruff>=0.7.4,<0.8.0", "ruff>=0.7.4,<0.8.0",
] ]
[project.scripts]
arcade-postgres = "arcade_postgres.__main__:main"
arcade_postgres = "arcade_postgres.__main__:main"
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = { path = "../../", editable = 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }
[tool.mypy] [tool.mypy]
files = [ "arcade_postgres/**/*.py",] files = [ "arcade_postgres/**/*.py",]
python_version = "3.10" python_version = "3.10"

View file

@ -1,8 +1,11 @@
import os import os
from os import environ from os import environ
from unittest.mock import MagicMock
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_postgres.tools.postgres import ( from arcade_postgres.tools.postgres import (
DatabaseEngine, DatabaseEngine,
discover_schemas, discover_schemas,
@ -10,8 +13,6 @@ from arcade_postgres.tools.postgres import (
execute_select_query, execute_select_query,
get_table_schema, get_table_schema,
) )
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
@ -23,14 +24,8 @@ POSTGRES_DATABASE_CONNECTION_STRING = (
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
context = ToolContext() context = MagicMock(spec=Context)
context.secrets = [] context.get_secret = MagicMock(return_value=POSTGRES_DATABASE_CONNECTION_STRING)
context.secrets.append(
ToolSecretItem(
key="POSTGRES_DATABASE_CONNECTION_STRING", value=POSTGRES_DATABASE_CONNECTION_STRING
)
)
return context return context

View file

@ -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 arcade_zendesk
app = MCPApp(
name="Zendesk",
instructions=(
"Use this server when you need to interact with Zendesk to help users "
"manage support tickets and search knowledge base articles."
),
)
app.add_tools_from_module(arcade_zendesk)
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()

View file

@ -2,6 +2,9 @@ import logging
from typing import Annotated, Any from typing import Annotated, Any
import httpx import httpx
from arcade_mcp_server import Context, tool
from arcade_mcp_server.auth import OAuth2
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import ( from arcade_mcp_server.metadata import (
Behavior, Behavior,
Classification, Classification,
@ -9,9 +12,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain, ServiceDomain,
ToolMetadata, ToolMetadata,
) )
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError
from arcade_zendesk.enums import ArticleSortBy, SortOrder from arcade_zendesk.enums import ArticleSortBy, SortOrder
from arcade_zendesk.utils import ( from arcade_zendesk.utils import (
@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
), ),
) )
async def search_articles( async def search_articles(
context: ToolContext, context: Context,
query: Annotated[ query: Annotated[
str | None, str | None,
"Search text to match against articles. Supports quoted expressions for exact matching", "Search text to match against articles. Supports quoted expressions for exact matching",

View file

@ -1,5 +1,7 @@
from typing import Annotated, Any from typing import Annotated, Any
from arcade_mcp_server import Context, tool
from arcade_mcp_server.auth import OAuth2
from arcade_mcp_server.metadata import ( from arcade_mcp_server.metadata import (
Behavior, Behavior,
Classification, Classification,
@ -7,8 +9,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain, ServiceDomain,
ToolMetadata, ToolMetadata,
) )
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_zendesk.who_am_i_util import build_who_am_i_response from arcade_zendesk.who_am_i_util import build_who_am_i_response
@ -30,7 +30,7 @@ from arcade_zendesk.who_am_i_util import build_who_am_i_response
), ),
) )
async def who_am_i( async def who_am_i(
context: ToolContext, context: Context,
) -> Annotated[ ) -> Annotated[
dict[str, Any], dict[str, Any],
"Get comprehensive user profile and Zendesk account information.", "Get comprehensive user profile and Zendesk account information.",

View file

@ -1,6 +1,9 @@
from typing import Annotated, Any from typing import Annotated, Any
import httpx import httpx
from arcade_mcp_server import Context, tool
from arcade_mcp_server.auth import OAuth2
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_mcp_server.metadata import ( from arcade_mcp_server.metadata import (
Behavior, Behavior,
Classification, Classification,
@ -8,9 +11,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain, ServiceDomain,
ToolMetadata, ToolMetadata,
) )
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError
from arcade_zendesk.enums import SortOrder, TicketStatus from arcade_zendesk.enums import SortOrder, TicketStatus
from arcade_zendesk.utils import fetch_paginated_results, get_zendesk_subdomain from arcade_zendesk.utils import fetch_paginated_results, get_zendesk_subdomain
@ -44,7 +44,7 @@ def _handle_ticket_not_found(response: httpx.Response, ticket_id: int) -> None:
), ),
) )
async def list_tickets( async def list_tickets(
context: ToolContext, context: Context,
status: Annotated[ status: Annotated[
TicketStatus, TicketStatus,
"The status of tickets to filter by. Defaults to 'open'", "The status of tickets to filter by. Defaults to 'open'",
@ -166,7 +166,7 @@ async def list_tickets(
), ),
) )
async def get_ticket_comments( async def get_ticket_comments(
context: ToolContext, context: Context,
ticket_id: Annotated[int, "The ID of the ticket to get comments for"], ticket_id: Annotated[int, "The ID of the ticket to get comments for"],
) -> Annotated[ ) -> Annotated[
dict[str, Any], "A dictionary containing the ticket comments, metadata, and ticket URL" dict[str, Any], "A dictionary containing the ticket comments, metadata, and ticket URL"
@ -229,7 +229,7 @@ async def get_ticket_comments(
), ),
) )
async def add_ticket_comment( async def add_ticket_comment(
context: ToolContext, context: Context,
ticket_id: Annotated[int, "The ID of the ticket to comment on"], ticket_id: Annotated[int, "The ID of the ticket to comment on"],
comment_body: Annotated[str, "The text of the comment"], comment_body: Annotated[str, "The text of the comment"],
public: Annotated[ public: Annotated[
@ -300,7 +300,7 @@ async def add_ticket_comment(
), ),
) )
async def mark_ticket_solved( async def mark_ticket_solved(
context: ToolContext, context: Context,
ticket_id: Annotated[int, "The ID of the ticket to mark as solved"], ticket_id: Annotated[int, "The ID of the ticket to mark as solved"],
comment_body: Annotated[ comment_body: Annotated[
str | None, str | None,

View file

@ -3,8 +3,8 @@ import re
from typing import Any from typing import Any
import httpx import httpx
from arcade_tdk import ToolContext from arcade_mcp_server import Context
from arcade_tdk.errors import ToolExecutionError from arcade_mcp_server.exceptions import ToolExecutionError
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -189,7 +189,7 @@ def validate_date_format(date_string: str) -> bool:
return False return False
def get_zendesk_subdomain(context: ToolContext) -> str: def get_zendesk_subdomain(context: Context) -> str:
""" """
Get the Zendesk subdomain from secrets with proper error handling. Get the Zendesk subdomain from secrets with proper error handling.

View file

@ -1,7 +1,7 @@
from typing import Any, TypedDict from typing import Any, TypedDict
import httpx import httpx
from arcade_tdk import ToolContext from arcade_mcp_server import Context
class WhoAmIResponse(TypedDict, total=False): class WhoAmIResponse(TypedDict, total=False):
@ -19,7 +19,7 @@ class WhoAmIResponse(TypedDict, total=False):
zendesk_access: bool zendesk_access: bool
async def build_who_am_i_response(context: ToolContext) -> WhoAmIResponse: async def build_who_am_i_response(context: Context) -> WhoAmIResponse:
"""Build comprehensive who am I response for Zendesk.""" """Build comprehensive who am I response for Zendesk."""
user_info = await _get_current_user(context) user_info = await _get_current_user(context)
organization_info = await _get_organization_info(context, user_info.get("organization_id")) organization_info = await _get_organization_info(context, user_info.get("organization_id"))
@ -32,7 +32,7 @@ async def build_who_am_i_response(context: ToolContext) -> WhoAmIResponse:
return response_data # type: ignore[return-value] return response_data # type: ignore[return-value]
async def _get_current_user(context: ToolContext) -> dict[str, Any]: async def _get_current_user(context: Context) -> dict[str, Any]:
"""Get current user information from Zendesk API.""" """Get current user information from Zendesk API."""
subdomain = context.get_secret("ZENDESK_SUBDOMAIN") subdomain = context.get_secret("ZENDESK_SUBDOMAIN")
base_url = f"https://{subdomain}.zendesk.com" base_url = f"https://{subdomain}.zendesk.com"
@ -48,9 +48,7 @@ async def _get_current_user(context: ToolContext) -> dict[str, Any]:
return response.json().get("user", {}) # type: ignore[no-any-return] return response.json().get("user", {}) # type: ignore[no-any-return]
async def _get_organization_info( async def _get_organization_info(context: Context, organization_id: int | None) -> dict[str, Any]:
context: ToolContext, organization_id: int | None
) -> dict[str, Any]:
"""Get organization information from Zendesk API.""" """Get organization information from Zendesk API."""
if not organization_id: if not organization_id:
return {} return {}

View file

@ -1,5 +1,6 @@
from datetime import timedelta from datetime import timedelta
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
DatetimeCritic, DatetimeCritic,
EvalRubric, EvalRubric,
@ -8,7 +9,6 @@ from arcade_evals import (
tool_eval, tool_eval,
) )
from arcade_evals.critic import BinaryCritic, SimilarityCritic from arcade_evals.critic import BinaryCritic, SimilarityCritic
from arcade_tdk import ToolCatalog
import arcade_zendesk import arcade_zendesk
from arcade_zendesk.enums import ArticleSortBy, SortOrder from arcade_zendesk.enums import ArticleSortBy, SortOrder

View file

@ -1,3 +1,4 @@
from arcade_core import ToolCatalog
from arcade_evals import ( from arcade_evals import (
BinaryCritic, BinaryCritic,
EvalRubric, EvalRubric,
@ -6,7 +7,6 @@ from arcade_evals import (
SimilarityCritic, SimilarityCritic,
tool_eval, tool_eval,
) )
from arcade_tdk import ToolCatalog
import arcade_zendesk import arcade_zendesk
from arcade_zendesk.enums import SortOrder, TicketStatus from arcade_zendesk.enums import SortOrder, TicketStatus

View file

@ -4,20 +4,21 @@ build-backend = "hatchling.build"
[project] [project]
name = "arcade_zendesk" name = "arcade_zendesk"
version = "0.4.0" version = "0.5.0"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"arcade-tdk>=3.0.0,<4.0.0",
"arcade-mcp-server>=1.17.0,<2.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"
] ]
[project.scripts]
arcade-zendesk = "arcade_zendesk.__main__:main"
arcade_zendesk = "arcade_zendesk.__main__:main"
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"arcade-mcp[all]>=1.2.0,<2.0.0", "arcade-mcp[all]>=1.2.0,<2.0.0",
"arcade-serve>=3.0.0,<4.0.0",
"pytest>=8.3.0,<8.4.0", "pytest>=8.3.0,<8.4.0",
"pytest-cov>=4.0.0,<4.1.0", "pytest-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0", "pytest-mock>=3.11.1,<3.12.0",
@ -31,8 +32,6 @@ dev = [
# Use local path sources for arcade libs when working locally # Use local path sources for arcade libs when working locally
[tool.uv.sources] [tool.uv.sources]
arcade-mcp = { path = "../../", editable = 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 } arcade-mcp-server = { path = "../../libs/arcade-mcp-server/", editable = true }

View file

@ -1,13 +1,13 @@
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from arcade_tdk import ToolContext from arcade_mcp_server import Context
@pytest.fixture @pytest.fixture
def mock_context(): def mock_context():
"""Standard mock context fixture used across all arcade toolkits.""" """Standard mock context fixture used across all arcade toolkits."""
context = MagicMock(spec=ToolContext) context = MagicMock(spec=Context)
context.get_auth_token_or_empty = MagicMock(return_value="fake-token") context.get_auth_token_or_empty = MagicMock(return_value="fake-token")
context.get_secret = MagicMock() context.get_secret = MagicMock()

View file

@ -1,5 +1,5 @@
import pytest import pytest
from arcade_tdk.errors import RetryableToolError, ToolExecutionError from arcade_mcp_server.exceptions import RetryableToolError, ToolExecutionError
from arcade_zendesk.enums import ArticleSortBy, SortOrder from arcade_zendesk.enums import ArticleSortBy, SortOrder
from arcade_zendesk.tools.search_articles import search_articles from arcade_zendesk.tools.search_articles import search_articles