diff --git a/README.md b/README.md index b8d66f6..d24cca2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ _AI Agents with Onchain Intelligence_ ## 📖 Overview -thirdweb AI is thirdweb's comprehensive toolkit for blockchain data analysis, wallet management, and AI agent interaction with blockchains. It simplifies complex blockchain operations into four core components: Insight for data analysis, Engine for wallet and contract operations, Storage for decentralized file management, and Nebula for natural language-powered blockchain interactions. +thirdweb AI is thirdweb's comprehensive toolkit for blockchain data analysis, wallet management, and AI agent interaction with blockchains. It simplifies complex blockchain operations into five core components: Insight for data analysis, Engine for wallet and contract operations, EngineCloud for cloud-based engine operations, Storage for decentralized file management, and Nebula for natural language-powered blockchain interactions. ## 🌐 Features @@ -23,6 +23,13 @@ Core blockchain interaction capabilities: - **Read**: Read operations for smart contracts and blockchain data - **Write**: Transaction creation and contract interaction +### EngineCloud +Cloud-based engine operations with advanced capabilities: +- **Server Wallets**: Create and manage server wallets with KMS integration +- **Contract Interaction**: Read from and write to smart contracts +- **Transaction Management**: Send transactions and query transaction history +- **Balance Queries**: Check native token balances on various chains + ### Storage Decentralized storage capabilities: - **Upload**: Upload files, directories, and JSON data to IPFS @@ -73,12 +80,13 @@ See the list of [supported framework and installation guides](python/thirdweb-ai #### Basic Usage ```python -from thirdweb_ai import Engine, Insight, Nebula, Storage, Tool +from thirdweb_ai import Engine, EngineCloud, Insight, Nebula, Storage, Tool # Initialize services insight = Insight(secret_key=...) nebula = Nebula(secret_key=...) engine = Engine(...) +engine_cloud = EngineCloud(secret_key=..., vault_access_token=...) # For cloud-based operations storage = Storage(secret_key=...) # Example: Create tools for AI agents @@ -92,6 +100,7 @@ tools = [ # tools = [ # *insight.get_tools(), # *engine.get_tools(), +# *engine_cloud.get_tools(), # *storage.get_tools(), # ] @@ -128,7 +137,7 @@ For non-security-related bugs, please use the GitHub issue tracker. ## ⚠️ Important Usage Notes -When using Nebula, do not combine it with other tools (Insight, Engine, Storage) in the same agent implementation as Nebula already calls these tools in the background. Using them together can lead to compatibility issues and unexpected behavior. +When using Nebula, do not combine it with other tools (Insight, Engine, EngineCloud, Storage) in the same agent implementation as Nebula already calls these tools in the background. Using them together can lead to compatibility issues and unexpected behavior. ## 📦 Publishing Workflow @@ -149,4 +158,4 @@ To publish a new version of thirdweb AI packages: ## 📝 License -thirdweb AI is licensed under the Apache-2.0 License. See the [LICENSE](./LICENSE) file for details. +thirdweb AI is licensed under the Apache-2.0 License. See the [LICENSE](./LICENSE) file for details. \ No newline at end of file diff --git a/python/thirdweb-ai/README.md b/python/thirdweb-ai/README.md index b759f74..ab56284 100644 --- a/python/thirdweb-ai/README.md +++ b/python/thirdweb-ai/README.md @@ -49,12 +49,13 @@ pip install "thirdweb-ai[pydantic-ai]" # For Pydantic AI thirdweb-ai provides a set of tools that can be integrated with various AI agent frameworks. Here's a basic example: ```python -from thirdweb_ai import Engine, Insight, Nebula, Storage, Tool +from thirdweb_ai import Engine, EngineCloud, Insight, Nebula, Storage, Tool # Initialize thirdweb services insight = Insight(secret_key=...) nebula = Nebula(secret_key=...) engine = Engine(secret_key=...) +engine_cloud = EngineCloud(secret_key=..., vault_access_token=...) # vault_access_token required for server wallet operations storage = Storage(secret_key=...) # Get available tools @@ -64,11 +65,22 @@ tools = [ *insight.get_tools(), *nebula.get_tools(), *engine.get_tools(), + *engine_cloud.get_tools(), # Use EngineCloud for cloud-based engine operations *storage.get_tools(), # Or pick an individual tool from the services ] ``` +### Available Services + +thirdweb-ai provides several core services: + +- **Engine**: Deploy contracts, manage wallets, execute transactions, and interact with smart contracts +- **EngineCloud**: Cloud-based engine operations for creating server wallets (with KMS integration), executing contract calls, and querying transaction history +- **Insight**: Query blockchain data, retrieve transactions, events, token balances, and contract metadata +- **Nebula**: Advanced onchain analytics and data processing +- **Storage**: Store and retrieve data using IPFS and other decentralized storage solutions + ## Framework Integration thirdweb-ai can be easily integrated with various AI agent frameworks. Below are a few examples of how to integrate with some of the supported frameworks: @@ -191,4 +203,4 @@ uv run ruff check . # Run type checking with pyright uv run pyright -``` +``` \ No newline at end of file diff --git a/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/__init__.py b/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/__init__.py new file mode 100644 index 0000000..af1ac1e --- /dev/null +++ b/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/__init__.py @@ -0,0 +1,3 @@ +from .google_adk import get_google_adk_tools + +__all__ = ["get_google_adk_tools"] diff --git a/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/google_adk.py b/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/google_adk.py index d23be3a..ac020e6 100644 --- a/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/google_adk.py +++ b/python/thirdweb-ai/src/thirdweb_ai/adapters/google_adk/google_adk.py @@ -40,8 +40,29 @@ def _get_declaration(self) -> types.FunctionDeclaration: Returns: A FunctionDeclaration for Google ADK """ - parameters = self.tool.schema["parameters"] - del parameters["additionalProperties"] + # Deep copy the parameters to avoid modifying the original + import copy + parameters = copy.deepcopy(self.tool.schema["parameters"]) + + if "additionalProperties" in parameters: + del parameters["additionalProperties"] + + def remove_additional_properties(obj: dict[str, Any]): + if "additionalProperties" in obj: + del obj["additionalProperties"] + + if "items" in obj and isinstance(obj["items"], dict): + remove_additional_properties(obj["items"]) + + if "properties" in obj and isinstance(obj["properties"], dict): + for prop in obj["properties"].values(): + if isinstance(prop, dict): + remove_additional_properties(prop) + + if "properties" in parameters: + for prop in parameters["properties"].values(): + remove_additional_properties(prop) + return types.FunctionDeclaration( name=self.name, description=self.description, diff --git a/python/thirdweb-ai/src/thirdweb_ai/services/__init__.py b/python/thirdweb-ai/src/thirdweb_ai/services/__init__.py index e69de29..6e413a5 100644 --- a/python/thirdweb-ai/src/thirdweb_ai/services/__init__.py +++ b/python/thirdweb-ai/src/thirdweb_ai/services/__init__.py @@ -0,0 +1,8 @@ +from thirdweb_ai.services.engine import Engine +from thirdweb_ai.services.engine_cloud import EngineCloud +from thirdweb_ai.services.insight import Insight +from thirdweb_ai.services.nebula import Nebula +from thirdweb_ai.services.service import Service +from thirdweb_ai.services.storage import Storage + +__all__ = ["Engine", "EngineCloud", "Insight", "Nebula", "Service", "Storage"] \ No newline at end of file diff --git a/python/thirdweb-ai/src/thirdweb_ai/services/engine_cloud.py b/python/thirdweb-ai/src/thirdweb_ai/services/engine_cloud.py new file mode 100644 index 0000000..45ac079 --- /dev/null +++ b/python/thirdweb-ai/src/thirdweb_ai/services/engine_cloud.py @@ -0,0 +1,220 @@ +# At the top of the file, add: +from typing import Annotated, Any, Literal, TypedDict + +from thirdweb_ai.services.service import Service +from thirdweb_ai.tools.tool import tool + + +class FilterField(TypedDict): + field: Literal["id", "batchIndex", "from", "signerAddress", "smartAccountAddress", "chainId"] + + +class FilterValues(TypedDict): + values: list[int] + + +class FilterOperator(TypedDict): + operator: Literal["AND", "OR"] + + +FilterCondition = FilterField | FilterValues | FilterOperator + + +class EngineCloud(Service): + def __init__( + self, + secret_key: str, + vault_access_token: str, + ): + super().__init__(base_url="https://engine.thirdweb.com/v1", secret_key=secret_key) + self.vault_access_token = vault_access_token + + def _make_headers(self): + headers = super()._make_headers() + if self.vault_access_token: + headers["x-vault-access-token"] = self.vault_access_token + return headers + + @tool( + description="Create a new engine server wallet. This is a helper route for creating a new EOA with your KMS provider, provided as a convenient alternative to creating an EOA directly with your KMS provider." + ) + def create_server_wallet( + self, + label: Annotated[ + str, + "A human-readable label to identify this wallet.", + ], + ) -> dict[str, Any]: + payload = {"label": label} + return self._post("accounts", payload) + + @tool( + description="Call a write function on a contract. This endpoint allows you to execute state-changing functions on smart contracts, with support for various execution strategies." + ) + def write_contract( + self, + from_address: Annotated[ + str, + "The address of the account to send the transaction from. Can be the address of a smart account or an EOA.", + ], + chain_id: Annotated[ + int, + "The numeric blockchain network ID where the contract is deployed (e.g., '1' for Ethereum mainnet, '137' for Polygon).", + ], + method: Annotated[str, "The name of the contract function to call on the contract."], + params: Annotated[list[Any], "The arguments to pass to the contract function."], + contract_address: Annotated[str, "The address of the smart contract to interact with."], + abi: Annotated[list[dict[str, Any]], "The ABI (Application Binary Interface) of the contract."], + value: Annotated[str, "The amount of native currency to send with the transaction (in wei)."] = "0", + ) -> dict[str, Any]: + payload = { + "executionOptions": { + "from": from_address, + "chainId": chain_id, + }, + "params": [ + { + "method": method, + "params": params, + "contractAddress": contract_address, + "abi": abi, + "value": value, + } + ], + } + return self._post("write/contract", payload) + + @tool( + description="Send an encoded transaction or a batch of transactions. This endpoint allows you to execute low-level transactions with raw transaction data." + ) + def send_transaction( + self, + from_address: Annotated[str, "The address of the account to send the transaction from."], + chain_id: Annotated[ + int, + "The numeric blockchain network ID where the transaction will be sent (e.g., '1' for Ethereum mainnet, '137' for Polygon).", + ], + to_address: Annotated[ + str, + "The recipient address for the transaction.", + ], + data: Annotated[ + str, + "The encoded transaction data (hexadecimal).", + ], + value: Annotated[ + str, + "The amount of native currency to send with the transaction (in wei).", + ] = "0", + ) -> dict[str, Any]: + payload = { + "executionOptions": { + "from": from_address, + "chainId": chain_id, + }, + "params": [ + { + "to": to_address, + "data": data, + "value": value, + } + ], + } + + return self._post("write/transaction", payload) + + @tool( + description="Call read-only contract functions or batch read using multicall. This is a gas-efficient way to query data from blockchain contracts without modifying state." + ) + def read_contract( + self, + multicall_address: Annotated[ + str | None, + "Optional multicall contract address for batching multiple calls. Defaults to the default multicall3 address for the chain", + ], + chain_id: Annotated[ + int, + "The numeric blockchain network ID where the contract is deployed (e.g., '1' for Ethereum mainnet, '137' for Polygon).", + ], + from_address: Annotated[str, "EVM address in hex format"], + method: Annotated[str, "The name of the contract function to call."], + params: Annotated[list[Any], "The arguments to pass to the contract function."], + contract_address: Annotated[ + str, + "The address of the smart contract to read from.", + ], + abi: Annotated[list[dict[str, Any]], "The ABI (Application Binary Interface) for the contract."], + ) -> dict[str, Any]: + payload = { + "readOptions": { + "multicallAddress": multicall_address, + "chainId": chain_id, + "from": from_address, + }, + "params": [ + { + "method": method, + "params": params, + "contractAddress": contract_address, + "abi": abi, + } + ], + } + + return self._post("read/contract", payload) + + @tool( + description="Fetch the native cryptocurrency balance (e.g., ETH, MATIC) for a given address on a specific blockchain." + ) + def get_native_balance( + self, + chain_id: Annotated[ + int, + "The numeric blockchain network ID to query (e.g., '1' for Ethereum mainnet, '137' for Polygon).", + ], + address: Annotated[str, "The wallet address to check the balance for."], + ) -> dict[str, Any]: + payload = { + "chainId": chain_id, + "address": address, + } + + return self._post("read/balance", payload) + + @tool( + description="Search for transactions with flexible filtering options. Retrieve transaction history with customizable filters for addresses, chains, statuses, and more." + ) + def search_transactions( + self, + filters: Annotated[FilterField, "List of filter conditions to apply"], + filters_operation: Annotated[ + Literal["AND", "OR"], + "Logical operation to apply between filters. 'AND' means all conditions must match, 'OR' means any condition can match.", + ] = "AND", + page: Annotated[ + int | None, + "Page number for paginated results, starting from 1.", + ] = 1, + limit: Annotated[ + int | None, + "Maximum number of transactions to return per page (1-100).", + ] = 20, + sort_by: Annotated[ + Literal["createdAt", "confirmedAt"], + "Field to sort results by.", + ] = "createdAt", + sort_direction: Annotated[ + Literal["asc", "desc"], + "Sort direction ('asc' for ascending, 'desc' for descending).", + ] = "desc", + ) -> dict[str, Any]: + payload = { + "filters": filters, + "filtersOperation": filters_operation, + "page": page, + "limit": limit, + "sortBy": sort_by, + "sortDirection": sort_direction, + } + + return self._post("transactions/search", payload) diff --git a/python/thirdweb-mcp/README.md b/python/thirdweb-mcp/README.md index 4d7b6ba..0879ad5 100644 --- a/python/thirdweb-mcp/README.md +++ b/python/thirdweb-mcp/README.md @@ -9,6 +9,7 @@ thirdweb MCP provides a unified interface to access thirdweb's suite of blockcha - **Nebula**: Autonomous onchain execution - real-time on-chain analysis, code generation and contract interactions - **Insight**: Blockchain data analysis capabilities for real-time on-chain data - **Engine**: Integration with thirdweb's backend infrastructure for contract deployments and interactions +- **EngineCloud**: Cloud-based engine operations for server wallets, contract interactions, and transaction management - **Storage**: Decentralized storage capabilities for uploading and retrieving data via IPFS ## Installation @@ -45,9 +46,10 @@ uv sync The thirdweb MCP server requires configuration based on which services you want to enable: -1. **thirdweb Secret Key**: Required for Nebula, Insight, and Storage services. Obtain from the [thirdweb dashboard](https://thirdweb.com/dashboard). +1. **thirdweb Secret Key**: Required for Nebula, Insight, Storage, and EngineCloud services. Obtain from the [thirdweb dashboard](https://thirdweb.com/dashboard). 2. **Chain IDs**: Blockchain network IDs to connect to (e.g., 1 for Ethereum mainnet, 137 for Polygon). 3. **Engine Configuration**: If using the Engine service, you'll need the Engine URL and authentication JWT. +4. **EngineCloud Configuration**: For EngineCloud operations, you may need the Vault Access Token for server wallet operations. You can provide these through command-line options or environment variables. @@ -66,7 +68,8 @@ THIRDWEB_SECRET_KEY=... thirdweb-mcp --transport sse --port 8080 THIRDWEB_SECRET_KEY=... thirdweb-mcp --chain-id 1 --chain-id 137 \ --engine-url YOUR_ENGINE_URL \ --engine-auth-jwt YOUR_ENGINE_JWT \ - --engine-backend-wallet-address YOUR_ENGINE_BACKEND_WALLET_ADDRESS + --engine-backend-wallet-address YOUR_ENGINE_BACKEND_WALLET_ADDRESS \ + --vault-access-token YOUR_VAULT_ACCESS_TOKEN ``` ### Environment variables @@ -77,6 +80,8 @@ You can also configure the MCP server using environment variables: - `THIRDWEB_ENGINE_URL`: URL endpoint for thirdweb Engine service - `THIRDWEB_ENGINE_AUTH_JWT`: Authentication JWT token for Engine - `THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS`: Wallet address for Engine backend +- `THIRDWEB_ENGINE_CLOUD_URL`: URL endpoint for EngineCloud service (defaults to https://engine.thirdweb.com/v1) +- `THIRDWEB_VAULT_ACCESS_TOKEN`: Vault access token for EngineCloud server wallet operations ### Integration with Claude Desktop To add this MCP server to Claude Desktop: @@ -100,7 +105,8 @@ To add this MCP server to Claude Desktop: "THIRDWEB_SECRET_KEY": "your thirdweb secret key from dashboard", "THIRDWEB_ENGINE_URL": "(OPTIONAL) your engine url", "THIRDWEB_ENGINE_AUTH_JWT": "(OPTIONAL) your engine auth jwt", - "THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS": "(OPTIONAL) your engine backend wallet address", + "THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS": "(OPTIONAL) your engine backend wallet address", + "THIRDWEB_VAULT_ACCESS_TOKEN": "(OPTIONAL) your vault access token for EngineCloud" }, } } @@ -142,6 +148,14 @@ Integrates with thirdweb's backend infrastructure: - Interact with deployed contracts - Manage wallet connections and transactions +### EngineCloud + +Cloud-based engine operations with advanced capabilities: +- Create and manage server wallets with KMS integration +- Read from and write to smart contracts +- Send transactions and query transaction history +- Check native token balances on various chains + ### Storage Provides decentralized storage functionality: @@ -155,4 +169,4 @@ Provides decentralized storage functionality: ## Support -For questions or support, please contact [support@thirdweb.com](mailto:support@thirdweb.com) or visit [thirdweb.com](https://thirdweb.com). +For questions or support, please contact [support@thirdweb.com](mailto:support@thirdweb.com) or visit [thirdweb.com](https://thirdweb.com). \ No newline at end of file diff --git a/python/thirdweb-mcp/src/mcp.py b/python/thirdweb-mcp/src/mcp.py index acfd5ce..f19239c 100644 --- a/python/thirdweb-mcp/src/mcp.py +++ b/python/thirdweb-mcp/src/mcp.py @@ -1,7 +1,7 @@ import os import click -from thirdweb_ai import Engine, Insight, Nebula, Storage +from thirdweb_ai import Engine, EngineCloud, Insight, Nebula, Storage from thirdweb_ai.adapters.mcp import add_fastmcp_tools from mcp.server.fastmcp import FastMCP @@ -54,6 +54,18 @@ default=lambda: os.getenv("THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS"), help="Wallet address used by the Engine backend for transactions. Optional for the 'engine' service. Falls back to THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS environment variable if not specified.", ) +@click.option( + "--engine-cloud-url", + type=str, + default=lambda: os.getenv("THIRDWEB_ENGINE_CLOUD_URL") or "https://engine.thirdweb.com/v1", + help="URL endpoint for thirdweb EngineCloud service. Falls back to THIRDWEB_ENGINE_CLOUD_URL environment variable if not specified.", +) +@click.option( + "--vault-access-token", + type=str, + default=lambda: os.getenv("THIRDWEB_VAULT_ACCESS_TOKEN"), + help="Access token for the vault service, required for certain EngineCloud operations like creating server wallets. Falls back to THIRDWEB_VAULT_ACCESS_TOKEN environment variable if not specified.", +) def main( port: int, transport: str, @@ -62,6 +74,8 @@ def main( engine_url: str, engine_auth_jwt: str, engine_backend_wallet_address: str | None, + engine_cloud_url: str, + vault_access_token: str | None, ): mcp = FastMCP("thirdweb MCP", port=port) @@ -70,7 +84,7 @@ def main( # determine which services to enable based on the provided options services = [] if secret_key: - services.extend(["nebula", "insight", "storage"]) + services.extend(["nebula", "insight", "storage", "engine_cloud"]) if engine_url and engine_auth_jwt: services.append("engine") @@ -102,9 +116,17 @@ def main( secret_key=secret_key or "", ) add_fastmcp_tools(mcp, engine.get_tools()) + + if "engine_cloud" in services: + engine_cloud = EngineCloud( + engine_cloud_url=engine_cloud_url, + secret_key=secret_key, + vault_access_token=vault_access_token, + ) + add_fastmcp_tools(mcp, engine_cloud.get_tools()) mcp.run(transport) if __name__ == "__main__": - main() + main() \ No newline at end of file