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

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "arcade_brightdata"
version = "0.3.0"
version = "0.4.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",
]
@ -16,10 +15,13 @@ dependencies = [
name = "meirk-brd"
email = "meirk@brightdata.com"
[project.scripts]
arcade-brightdata = "arcade_brightdata.__main__:main"
arcade_brightdata = "arcade_brightdata.__main__:main"
[project.optional-dependencies]
dev = [
"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-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0",
@ -48,8 +50,6 @@ ignore_missing_imports = "True"
[tool.uv.sources]
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]

View file

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

View file

@ -1,8 +1,8 @@
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_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError
from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
@ -20,7 +20,7 @@ from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
),
)
async def discover_schemas(
context: ToolContext,
context: Context,
) -> list[str]:
"""Discover all the schemas in the ClickHouse database.
@ -43,7 +43,7 @@ async def discover_schemas(
),
)
async def discover_databases(
context: ToolContext,
context: Context,
) -> list[str]:
"""Discover all the databases in the ClickHouse database."""
async with await DatabaseEngine.get_engine(
@ -66,7 +66,7 @@ async def discover_databases(
),
)
async def discover_tables(
context: ToolContext,
context: Context,
) -> list[str]:
"""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(
context: ToolContext,
context: Context,
schema_name: Annotated[str, "The schema to get the table schema of"],
table_name: Annotated[str, "The table to get the schema of"],
) -> list[str]:
@ -120,7 +120,7 @@ async def get_table_schema(
),
)
async def execute_select_query(
context: ToolContext,
context: Context,
select_clause: Annotated[
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.",

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "arcade_clickhouse"
version = "0.2.0"
version = "0.3.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",
@ -22,11 +21,9 @@ dependencies = [
name = "evantahler"
email = "support@arcade.dev"
[project.optional-dependencies]
dev = [
"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-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0",
@ -37,14 +34,15 @@ dev = [
"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
[tool.uv.sources]
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]
files = [ "arcade_clickhouse/**/*.py",]
python_version = "3.10"

View file

@ -1,5 +1,6 @@
import os
from os import environ
from unittest.mock import MagicMock
import pytest
import pytest_asyncio
@ -10,8 +11,8 @@ from arcade_clickhouse.tools.clickhouse import (
execute_select_query,
get_table_schema,
)
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
CLICKHOUSE_DATABASE_CONNECTION_STRING = (
environ.get("TEST_CLICKHOUSE_DATABASE_CONNECTION_STRING")
@ -21,14 +22,8 @@ CLICKHOUSE_DATABASE_CONNECTION_STRING = (
@pytest.fixture
def mock_context():
context = ToolContext()
context.secrets = []
context.secrets.append(
ToolSecretItem(
key="CLICKHOUSE_DATABASE_CONNECTION_STRING", value=CLICKHOUSE_DATABASE_CONNECTION_STRING
)
)
context = MagicMock(spec=Context)
context.get_secret = MagicMock(return_value=CLICKHOUSE_DATABASE_CONNECTION_STRING)
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 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 (
Behavior,
Classification,
@ -7,9 +10,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain,
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
@ -32,7 +32,7 @@ from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linked
),
)
async def create_text_post(
context: ToolContext,
context: Context,
text: Annotated[str, "The text content of the post"],
) -> Annotated[str, "URL of the shared post"]:
"""Share a new text post to LinkedIn."""

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
from unittest.mock import AsyncMock, MagicMock
import pytest
from arcade_tdk.errors import ToolExecutionError
from arcade_mcp_server.exceptions import ToolExecutionError
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 typing import Annotated
from arcade_mcp_server import tool
from arcade_mcp_server.metadata import Behavior, ToolMetadata
from arcade_tdk import tool
decimal.getcontext().prec = 100

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "arcade_math"
version = "1.1.0"
version = "1.2.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]]
@ -18,7 +17,6 @@ email = "dev@arcade.dev"
[project.optional-dependencies]
dev = [
"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-cov>=4.0.0,<4.1.0",
"pytest-asyncio>=0.24.0,<0.25.0",
@ -29,11 +27,13 @@ dev = [
"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
[tool.uv.sources]
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]

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import pytest
from arcade_tdk.errors import ToolExecutionError
from arcade_mcp_server.exceptions import ToolExecutionError
from arcade_math.tools.rational import (
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 arcade_tdk.errors import RetryableToolError
from arcade_mcp_server.exceptions import RetryableToolError
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
from pymongo.errors import ServerSelectionTimeoutError

View file

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

View file

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

View file

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

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "arcade_mongodb"
version = "0.2.0"
version = "0.3.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",
@ -18,11 +17,9 @@ dependencies = [
name = "evantahler"
email = "support@arcade.dev"
[project.optional-dependencies]
dev = [
"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-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0",
@ -33,14 +30,15 @@ dev = [
"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
[tool.uv.sources]
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]
files = [ "arcade_mongodb/**/*.py",]
python_version = "3.10"

View file

@ -1,19 +1,18 @@
from unittest.mock import MagicMock
import pytest
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_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from .conftest import TEST_MONGODB_CONNECTION_STRING
@pytest.fixture
def mock_context():
context = ToolContext()
context.secrets = []
context.secrets.append(
ToolSecretItem(key="MONGODB_CONNECTION_STRING", value=TEST_MONGODB_CONNECTION_STRING)
)
context = MagicMock(spec=Context)
context.get_secret = MagicMock(return_value=TEST_MONGODB_CONNECTION_STRING)
return context

View file

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

View file

@ -1,18 +1,17 @@
from unittest.mock import MagicMock
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_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from .conftest import TEST_MONGODB_CONNECTION_STRING
@pytest.fixture
def mock_context():
context = ToolContext()
context.secrets = []
context.secrets.append(
ToolSecretItem(key="MONGODB_CONNECTION_STRING", value=TEST_MONGODB_CONNECTION_STRING)
)
context = MagicMock(spec=Context)
context.get_secret = MagicMock(return_value=TEST_MONGODB_CONNECTION_STRING)
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 urllib.parse import urlparse
from arcade_tdk.errors import RetryableToolError
from arcade_mcp_server.exceptions import RetryableToolError
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine

View file

@ -1,8 +1,8 @@
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_tdk import ToolContext, tool
from arcade_tdk.errors import RetryableToolError
from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import AsyncEngine
@ -22,7 +22,7 @@ from ..database_engine import MAX_ROWS_RETURNED, DatabaseEngine
),
)
async def discover_schemas(
context: ToolContext,
context: Context,
) -> list[str]:
"""Discover all the schemas in the postgres database."""
async with await DatabaseEngine.get_engine(
@ -45,7 +45,7 @@ async def discover_schemas(
),
)
async def discover_tables(
context: ToolContext,
context: Context,
schema_name: Annotated[
str, "The database schema to discover tables in (default value: 'public')"
] = "public",
@ -74,7 +74,7 @@ async def discover_tables(
),
)
async def get_table_schema(
context: ToolContext,
context: Context,
schema_name: Annotated[str, "The database schema to get the table schema of"],
table_name: Annotated[str, "The table to get the schema of"],
) -> list[str]:
@ -102,7 +102,7 @@ async def get_table_schema(
),
)
async def execute_select_query(
context: ToolContext,
context: Context,
select_clause: Annotated[
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.",

View file

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

View file

@ -4,11 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "arcade_postgres"
version = "0.4.0"
version = "0.5.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",
@ -21,11 +20,9 @@ dependencies = [
name = "evantahler"
email = "support@arcade.dev"
[project.optional-dependencies]
dev = [
"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-cov>=4.0.0,<4.1.0",
"pytest-mock>=3.11.1,<3.12.0",
@ -36,14 +33,15 @@ dev = [
"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
[tool.uv.sources]
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]
files = [ "arcade_postgres/**/*.py",]
python_version = "3.10"

View file

@ -1,8 +1,11 @@
import os
from os import environ
from unittest.mock import MagicMock
import pytest
import pytest_asyncio
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import RetryableToolError
from arcade_postgres.tools.postgres import (
DatabaseEngine,
discover_schemas,
@ -10,8 +13,6 @@ from arcade_postgres.tools.postgres import (
execute_select_query,
get_table_schema,
)
from arcade_tdk import ToolContext, ToolSecretItem
from arcade_tdk.errors import RetryableToolError
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
@ -23,14 +24,8 @@ POSTGRES_DATABASE_CONNECTION_STRING = (
@pytest.fixture
def mock_context():
context = ToolContext()
context.secrets = []
context.secrets.append(
ToolSecretItem(
key="POSTGRES_DATABASE_CONNECTION_STRING", value=POSTGRES_DATABASE_CONNECTION_STRING
)
)
context = MagicMock(spec=Context)
context.get_secret = MagicMock(return_value=POSTGRES_DATABASE_CONNECTION_STRING)
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
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 (
Behavior,
Classification,
@ -9,9 +12,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain,
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.utils import (
@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
),
)
async def search_articles(
context: ToolContext,
context: Context,
query: Annotated[
str | None,
"Search text to match against articles. Supports quoted expressions for exact matching",

View file

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

View file

@ -1,6 +1,9 @@
from typing import Annotated, Any
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 (
Behavior,
Classification,
@ -8,9 +11,6 @@ from arcade_mcp_server.metadata import (
ServiceDomain,
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.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(
context: ToolContext,
context: Context,
status: Annotated[
TicketStatus,
"The status of tickets to filter by. Defaults to 'open'",
@ -166,7 +166,7 @@ async def list_tickets(
),
)
async def get_ticket_comments(
context: ToolContext,
context: Context,
ticket_id: Annotated[int, "The ID of the ticket to get comments for"],
) -> Annotated[
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(
context: ToolContext,
context: Context,
ticket_id: Annotated[int, "The ID of the ticket to comment on"],
comment_body: Annotated[str, "The text of the comment"],
public: Annotated[
@ -300,7 +300,7 @@ async def add_ticket_comment(
),
)
async def mark_ticket_solved(
context: ToolContext,
context: Context,
ticket_id: Annotated[int, "The ID of the ticket to mark as solved"],
comment_body: Annotated[
str | None,

View file

@ -3,8 +3,8 @@ import re
from typing import Any
import httpx
from arcade_tdk import ToolContext
from arcade_tdk.errors import ToolExecutionError
from arcade_mcp_server import Context
from arcade_mcp_server.exceptions import ToolExecutionError
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
@ -189,7 +189,7 @@ def validate_date_format(date_string: str) -> bool:
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.

View file

@ -1,7 +1,7 @@
from typing import Any, TypedDict
import httpx
from arcade_tdk import ToolContext
from arcade_mcp_server import Context
class WhoAmIResponse(TypedDict, total=False):
@ -19,7 +19,7 @@ class WhoAmIResponse(TypedDict, total=False):
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."""
user_info = await _get_current_user(context)
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]
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."""
subdomain = context.get_secret("ZENDESK_SUBDOMAIN")
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]
async def _get_organization_info(
context: ToolContext, organization_id: int | None
) -> dict[str, Any]:
async def _get_organization_info(context: Context, organization_id: int | None) -> dict[str, Any]:
"""Get organization information from Zendesk API."""
if not organization_id:
return {}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
from unittest.mock import AsyncMock, MagicMock
import pytest
from arcade_tdk import ToolContext
from arcade_mcp_server import Context
@pytest.fixture
def mock_context():
"""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_secret = MagicMock()

View file

@ -1,5 +1,5 @@
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.tools.search_articles import search_articles