# Directory Structure ``` ├── _photos │ └── roast.png ├── .env.example ├── .gitignore ├── .python-version ├── LICENSE ├── main.py ├── pyproject.toml ├── README.md ├── requirements.txt ├── test.py ├── up_mcp.py ├── uv.lock └── weather.py ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.13 2 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | UP_TOKEN="up:yeah:YOUR_UP_TOKEN_HERE" ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | __pycache__/ 2 | .DS_Store 3 | .venv/ 4 | .mypy_cache/ 5 | .pytest_cache/ 6 | .ruff_cache/ 7 | .vscode/ 8 | .env 9 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Up Bank MCP Server 2 | 3 | MCP wrapper of the Python Up Bank API Wrapper [up-bank-api](https://github.com/jcwillox/up-bank-api) (credit to [@jcwillox](https://github.com/jcwillox)) (it's wrappers all the way down) 4 | 5 | Made for Claude, by Claude (lol) 6 | 7 | This is a Model Context Protocol (MCP) server that provides tools for interacting with the Up Bank API. It allows you to manage accounts, transactions, categories, tags, and webhooks through MCP-enabled clients like Claude for Desktop. 8 | 9 | Feel free to contribute if you want to better optimise it for LLM, etc. However it's fine as is. I just wanted to learn how to make an MCP server. 10 | 11 | Now Claude can roast me for my transaction history, what else is it good for? 12 | 13 |  14 | **Figure: Too much takeout I guess** 15 | 16 | ## Prerequisites 17 | 18 | - Python 3.10 or higher 19 | - Up Bank API token 20 | - `uv` package manager 21 | 22 | ## Installation 23 | 24 | 1. First, install `uv` by running: 25 | 26 | ```bash 27 | curl -LsSf https://astral.sh/uv/install.sh | sh 28 | ``` 29 | 30 | Make sure to restart your terminal after installing `uv`. 31 | 32 | 2. Clone this repository and navigate to it: 33 | 34 | ```bash 35 | git clone <repository-url> 36 | cd up-mcp 37 | ``` 38 | 39 | 3. Create and activate a virtual environment: 40 | 41 | ```bash 42 | uv venv 43 | source .venv/bin/activate # On Windows, use: .venv\Scripts\activate 44 | ``` 45 | 46 | 4. Install dependencies: 47 | 48 | ```bash 49 | uv pip install -r requirements.txt 50 | ``` 51 | 52 | ### Using with Claude for Desktop or other MCP-enabled clients 53 | 54 | 1. Open your Claude (or other MCP-enabled) configuration file: 55 | - macOS/Linux: `~/Library/Application Support/Claude/claude_desktop_config.json` 56 | - Windows: `%AppData%\Claude\claude_desktop_config.json` 57 | 58 | 2. Add the server configuration: 59 | 60 | ```json 61 | { 62 | "mcpServers": { 63 | "up-mcp": { 64 | "command": "/ABSOLUTE/PATH/TO/uv", 65 | "args": [ 66 | "--directory", 67 | "/ABSOLUTE/PATH/TO/up-mcp", 68 | "run", 69 | "up_mcp.py" 70 | ], 71 | "env": { 72 | "UP_TOKEN": "up:yeah:your-token-here" 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | Replace `/ABSOLUTE/PATH/TO/uv` with the absolute path to your `uv` executable. 80 | Replace `/ABSOLUTE/PATH/TO/up-mcp` with the absolute path to your project directory. 81 | Replace `up:yeah:your-token-here` with your Up Bank API token. 82 | 83 | Get your Up Bank API token from the [Up Bank website (https://api.up.com.au/)](https://api.up.com.au/). 84 | 85 | 3. Restart Claude for Desktop. 86 | 87 | ## Available Tools 88 | 89 | The server provides the following tools: 90 | - Account management (get accounts, get specific account) 91 | - Transaction management (get transactions, get specific transaction) 92 | - Category management (get categories, categorize transactions) 93 | - Tag management (get tags, add/remove tags from transactions) 94 | - Webhook management (create, delete, ping webhooks) 95 | 96 | ## Testing 97 | 98 | You can test the server using the included `test.py` script: 99 | 100 | ```bash 101 | python test.py 102 | ``` 103 | 104 | This will run through basic functionality tests including account retrieval and transaction listing. 105 | 106 | ## Troubleshooting 107 | 108 | If you encounter issues: 109 | 110 | 1. Verify your UP_TOKEN environment variable is set correctly 111 | 2. Check that all dependencies are installed correctly 112 | 3. Ensure you're using Python 3.10 or higher 113 | 4. Check Claude's logs for MCP-related issues ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | mcp[cli] 2 | httpx 3 | up-bank-api 4 | pytest-asyncio ``` -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- ```python 1 | import weather 2 | 3 | def main(): 4 | weather.mcp.run(transport='stdio') 5 | 6 | 7 | if __name__ == "__main__": 8 | main() 9 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "up-mcp" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.6.0", 10 | ] 11 | ``` -------------------------------------------------------------------------------- /weather.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Any 2 | import httpx 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | # Initialize FastMCP server 6 | mcp = FastMCP("weather") 7 | 8 | # Constants 9 | NWS_API_BASE = "https://api.weather.gov" 10 | USER_AGENT = "weather-app/1.0" 11 | 12 | 13 | async def make_nws_request(url: str) -> dict[str, Any] | None: 14 | """Make a request to the NWS API with proper error handling.""" 15 | headers = { 16 | "User-Agent": USER_AGENT, 17 | "Accept": "application/geo+json" 18 | } 19 | async with httpx.AsyncClient() as client: 20 | try: 21 | response = await client.get(url, headers=headers, timeout=30.0) 22 | response.raise_for_status() 23 | return response.json() 24 | except Exception: 25 | return None 26 | 27 | def format_alert(feature: dict) -> str: 28 | """Format an alert feature into a readable string.""" 29 | props = feature["properties"] 30 | return f""" 31 | Event: {props.get('event', 'Unknown')} 32 | Area: {props.get('areaDesc', 'Unknown')} 33 | Severity: {props.get('severity', 'Unknown')} 34 | Description: {props.get('description', 'No description available')} 35 | Instructions: {props.get('instruction', 'No specific instructions provided')} 36 | """ 37 | 38 | @mcp.tool() 39 | async def get_alerts(state: str) -> str: 40 | """Get weather alerts for a US state. 41 | 42 | Args: 43 | state: Two-letter US state code (e.g. CA, NY) 44 | """ 45 | url = f"{NWS_API_BASE}/alerts/active/area/{state}" 46 | data = await make_nws_request(url) 47 | 48 | if not data or "features" not in data: 49 | return "Unable to fetch alerts or no alerts found." 50 | 51 | if not data["features"]: 52 | return "No active alerts for this state." 53 | 54 | alerts = [format_alert(feature) for feature in data["features"]] 55 | return "\n---\n".join(alerts) 56 | 57 | @mcp.tool() 58 | async def get_forecast(latitude: float, longitude: float) -> str: 59 | """Get weather forecast for a location. 60 | 61 | Args: 62 | latitude: Latitude of the location 63 | longitude: Longitude of the location 64 | """ 65 | # First get the forecast grid endpoint 66 | points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" 67 | points_data = await make_nws_request(points_url) 68 | 69 | if not points_data: 70 | return "Unable to fetch forecast data for this location." 71 | 72 | # Get the forecast URL from the points response 73 | forecast_url = points_data["properties"]["forecast"] 74 | forecast_data = await make_nws_request(forecast_url) 75 | 76 | if not forecast_data: 77 | return "Unable to fetch detailed forecast." 78 | 79 | # Format the periods into a readable forecast 80 | periods = forecast_data["properties"]["periods"] 81 | forecasts = [] 82 | for period in periods[:5]: # Only show next 5 periods 83 | forecast = f""" 84 | {period['name']}: 85 | Temperature: {period['temperature']}°{period['temperatureUnit']} 86 | Wind: {period['windSpeed']} {period['windDirection']} 87 | Forecast: {period['detailedForecast']} 88 | """ 89 | forecasts.append(forecast) 90 | 91 | return "\n---\n".join(forecasts) 92 | 93 | if __name__ == "__main__": 94 | # Initialize and run the server 95 | mcp.run(transport='stdio') ``` -------------------------------------------------------------------------------- /up_mcp.py: -------------------------------------------------------------------------------- ```python 1 | from upbankapi import AsyncClient, NotAuthorizedException 2 | from typing import Any, Optional, Union 3 | from datetime import datetime, timedelta 4 | from mcp.server.fastmcp import FastMCP 5 | import os 6 | # Initialize FastMCP server 7 | mcp = FastMCP("up-mcp") 8 | 9 | 10 | UP_TOKEN = os.getenv("UP_TOKEN") 11 | 12 | @mcp.tool() 13 | async def get_user_id() -> str: 14 | """Get the user ID for the UP API. 15 | """ 16 | 17 | async with AsyncClient(token=UP_TOKEN) as client: 18 | try: 19 | user_id = await client.ping() 20 | return f"Authorized: {user_id}" 21 | except NotAuthorizedException: 22 | return "The token is invalid" 23 | 24 | @mcp.tool() 25 | async def get_accounts() -> list[dict[str, Any]]: 26 | """Get all accounts for the user. 27 | """ 28 | async with AsyncClient(token=UP_TOKEN) as client: 29 | accounts = await client.accounts() 30 | account_list = [] 31 | async for account in accounts: 32 | account_list.append({ 33 | "id": account.id, 34 | "name": account.name, 35 | "balance": account.balance 36 | }) 37 | return account_list 38 | 39 | @mcp.tool() 40 | async def get_account(id: str) -> str: 41 | """Get an account for the user. 42 | 43 | Args: 44 | id: The ID of the account to get. 45 | """ 46 | async with AsyncClient(token=UP_TOKEN) as client: 47 | account = await client.account(id) 48 | return str(account) 49 | 50 | @mcp.tool() 51 | async def get_categories(parent_id: Optional[str] = None) -> list[dict[str, Any]]: 52 | """Get all categories or categories under a specific parent. 53 | 54 | Args: 55 | parent_id: Optional ID of the parent category to filter by. 56 | """ 57 | async with AsyncClient(token=UP_TOKEN) as client: 58 | categories = await client.categories(parent=parent_id) 59 | return [{"id": cat.id, "name": cat.name} for cat in categories] 60 | 61 | @mcp.tool() 62 | async def get_category(category_id: str) -> str: 63 | """Get a specific category by ID. 64 | 65 | Args: 66 | category_id: The ID of the category to get. 67 | """ 68 | async with AsyncClient(token=UP_TOKEN) as client: 69 | category = await client.category(category_id) 70 | return str(category) 71 | 72 | @mcp.tool() 73 | async def categorize_transaction(transaction_id: str, category_id: Optional[str]) -> bool: 74 | """Categorize a transaction. 75 | 76 | Args: 77 | transaction_id: The ID of the transaction to categorize. 78 | category_id: The category ID to assign, or None to remove categorization. 79 | """ 80 | async with AsyncClient(token=UP_TOKEN) as client: 81 | return await client.categorize(transaction_id, category_id) 82 | 83 | @mcp.tool() 84 | async def get_tags() -> list[dict[str, Any]]: 85 | """Get all tags for the user.""" 86 | async with AsyncClient(token=UP_TOKEN) as client: 87 | tags = await client.tags() 88 | tag_list = [] 89 | async for tag in tags: 90 | tag_list.append({"id": tag.id, "name": tag.name}) 91 | return tag_list 92 | 93 | @mcp.tool() 94 | async def add_transaction_tags(transaction_id: str, tags: list[str]) -> bool: 95 | """Add tags to a transaction. 96 | 97 | Args: 98 | transaction_id: The ID of the transaction. 99 | tags: List of tag IDs to add. 100 | """ 101 | async with AsyncClient(token=UP_TOKEN) as client: 102 | return await client.add_tags(transaction_id, *tags) 103 | 104 | @mcp.tool() 105 | async def remove_transaction_tags(transaction_id: str, tags: list[str]) -> bool: 106 | """Remove tags from a transaction. 107 | 108 | Args: 109 | transaction_id: The ID of the transaction. 110 | tags: List of tag IDs to remove. 111 | """ 112 | async with AsyncClient(token=UP_TOKEN) as client: 113 | return await client.remove_tags(transaction_id, *tags) 114 | 115 | @mcp.tool() 116 | async def get_transaction(transaction_id: str) -> str: 117 | """Get a specific transaction by ID. 118 | 119 | Args: 120 | transaction_id: The ID of the transaction to get. 121 | """ 122 | async with AsyncClient(token=UP_TOKEN) as client: 123 | transaction = await client.transaction(transaction_id) 124 | return str(transaction) 125 | 126 | @mcp.tool() 127 | async def get_transactions( 128 | account_id: Optional[str] = None, 129 | status: Optional[str] = None, 130 | since: Optional[datetime] = datetime.now() - timedelta(days=7), 131 | until: Optional[datetime] = None, 132 | category_id: Optional[str] = None, 133 | tag_id: Optional[str] = None, 134 | verbose: bool = False 135 | ) -> list[dict[str, Any]]: 136 | """Get transactions with optional filters. 137 | 138 | Args: 139 | account_id: Optional account ID to filter by. 140 | status: Optional transaction status to filter by. 141 | since: Optional start datetime (defaults to 7 days ago - longer may take longer to load) 142 | until: Optional end datetime. 143 | category_id: Optional category ID to filter by. 144 | tag_id: Optional tag ID to filter by. 145 | """ 146 | async with AsyncClient(token=UP_TOKEN) as client: 147 | transactions = await client.transactions( 148 | account=account_id, 149 | status=status, 150 | since=since, 151 | until=until, 152 | category=category_id, 153 | tag=tag_id 154 | ) 155 | transaction_list = [] 156 | if verbose: 157 | async for tx in transactions: 158 | transaction_list.append({ 159 | "id": tx.id, 160 | "description": tx.description, 161 | "amount": tx.amount, 162 | "status": tx.status, 163 | "created_at": tx.created_at 164 | }) 165 | else: 166 | async for tx in transactions: 167 | transaction_list.append({ 168 | "description": tx.description, 169 | "amount": tx.amount, 170 | }) 171 | return transaction_list 172 | 173 | @mcp.tool() 174 | async def get_webhooks() -> list[dict[str, Any]]: 175 | """Get all webhooks for the user.""" 176 | async with AsyncClient(token=UP_TOKEN) as client: 177 | webhooks = await client.webhooks() 178 | webhook_list = [] 179 | async for webhook in webhooks: 180 | webhook_list.append({ 181 | "id": webhook.id, 182 | "url": webhook.url, 183 | "description": webhook.description 184 | }) 185 | return webhook_list 186 | 187 | @mcp.tool() 188 | async def create_webhook(url: str, description: Optional[str] = None) -> dict[str, Any]: 189 | """Create a new webhook. 190 | 191 | Args: 192 | url: The URL that this webhook should post events to. 193 | """ 194 | async with AsyncClient(token=UP_TOKEN) as client: 195 | webhook = await client.webhook.create(url, description) 196 | return { 197 | "id": webhook.id, 198 | "url": webhook.url, 199 | "description": webhook.description, 200 | "secret_key": webhook.secret_key, 201 | "created_at": webhook.created_at 202 | } 203 | 204 | @mcp.tool() 205 | async def delete_webhook(webhook_id: str) -> bool: 206 | """Delete a webhook. 207 | 208 | Args: 209 | webhook_id: The ID of the webhook to delete. 210 | """ 211 | async with AsyncClient(token=UP_TOKEN) as client: 212 | return await client.webhook.delete(webhook_id) 213 | 214 | @mcp.tool() 215 | async def ping_webhook(webhook_id: str) -> str: 216 | """Ping a webhook. 217 | 218 | Args: 219 | webhook_id: The ID of the webhook to ping. 220 | """ 221 | async with AsyncClient(token=UP_TOKEN) as client: 222 | event = await client.webhook.ping(webhook_id) 223 | return str(event) 224 | 225 | if __name__ == "__main__": 226 | mcp.run(transport='stdio') 227 | ``` -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- ```python 1 | import pytest 2 | import asyncio 3 | from datetime import datetime, timedelta 4 | import os 5 | from typing import AsyncGenerator 6 | 7 | import up_mcp 8 | from upbankapi import AsyncClient, NotAuthorizedException 9 | 10 | # Fixture to ensure UP_TOKEN is set 11 | @pytest.fixture(scope="session", autouse=True) 12 | def check_token(): 13 | token = os.getenv("UP_TOKEN") 14 | if not token: 15 | pytest.skip("UP_TOKEN environment variable not set") 16 | return token 17 | 18 | # Fixture for async tests 19 | @pytest.fixture(scope="session") 20 | def event_loop(): 21 | loop = asyncio.get_event_loop() 22 | yield loop 23 | loop.close() 24 | 25 | @pytest.fixture(scope="session") 26 | async def client(check_token) -> AsyncGenerator[AsyncClient, None]: 27 | async with AsyncClient(token=check_token) as client: 28 | yield client 29 | 30 | @pytest.mark.asyncio 31 | async def test_get_user_id(): 32 | """Test that we can authenticate and get a user ID.""" 33 | result = await up_mcp.get_user_id() 34 | assert result is not None 35 | assert isinstance(result, str) 36 | 37 | # Extract the actual user ID from the result 38 | # The format is "Authorized: {user_id}" 39 | assert result.startswith("Authorized: ") 40 | user_id = result.split("Authorized: ")[1] 41 | 42 | # Check that the user_id is a valid UUID format 43 | assert isinstance(user_id, str) 44 | # Optional: validate UUID format if needed 45 | import uuid 46 | try: 47 | uuid_obj = uuid.UUID(user_id) 48 | assert str(uuid_obj) == user_id # Ensures it's a valid UUID string 49 | except ValueError: 50 | pytest.fail(f"User ID '{user_id}' is not a valid UUID") 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_get_accounts(): 55 | """Test retrieving all accounts.""" 56 | accounts = await up_mcp.get_accounts() 57 | assert accounts is not None 58 | assert isinstance(accounts, list) 59 | assert len(accounts) > 0 60 | 61 | # Check account structure based on actual response 62 | account = accounts[0] 63 | assert "id" in account 64 | assert "name" in account 65 | assert "balance" in account 66 | assert isinstance(account["id"], str) 67 | assert isinstance(account["name"], str) 68 | assert isinstance(account["balance"], (int, float)) 69 | 70 | 71 | @pytest.mark.asyncio 72 | async def test_get_specific_account(): 73 | """Test retrieving a specific account by ID.""" 74 | # First get all accounts 75 | accounts = await up_mcp.get_accounts() 76 | account_id = accounts[0]["id"] 77 | 78 | # Then get specific account 79 | account = await up_mcp.get_account(account_id) 80 | assert account is not None 81 | 82 | # Check the structure of the response 83 | # The actual response is formatted like: <Account 'Spending' (TRANSACTIONAL): 9.29 AUD> 84 | assert isinstance(account, str) 85 | assert account.startswith("<Account ") 86 | assert account.endswith(">") 87 | 88 | # Check that the account name from the first response is in the specific account response 89 | account_name = accounts[0]["name"] 90 | assert account_name in account 91 | 92 | 93 | @pytest.mark.asyncio 94 | async def test_get_transactions(): 95 | """Test retrieving transactions with various filters.""" 96 | # Test with date filter 97 | since_date = datetime.now() - timedelta(days=7) 98 | transactions = await up_mcp.get_transactions(since=since_date) 99 | assert transactions is not None 100 | assert isinstance(transactions, list) 101 | 102 | if len(transactions) > 0: 103 | # Check transaction structure based on actual response 104 | transaction = transactions[0] 105 | assert isinstance(transaction, dict) 106 | assert "description" in transaction 107 | assert "amount" in transaction 108 | assert isinstance(transaction["description"], str) 109 | assert isinstance(transaction["amount"], (int, float)) 110 | 111 | # Test with verbose parameter 112 | verbose_transactions = await up_mcp.get_transactions(since=since_date, verbose=True) 113 | if len(verbose_transactions) > 0: 114 | verbose_tx = verbose_transactions[0] 115 | assert "id" in verbose_tx 116 | assert "status" in verbose_tx 117 | assert "created_at" in verbose_tx 118 | 119 | 120 | @pytest.mark.asyncio 121 | async def test_get_categories(): 122 | """Test retrieving categories.""" 123 | categories = await up_mcp.get_categories() 124 | assert categories is not None 125 | assert isinstance(categories, list) 126 | 127 | if len(categories) > 0: 128 | # Check category structure based on actual response 129 | category = categories[0] 130 | assert "id" in category 131 | assert "name" in category 132 | assert isinstance(category["id"], str) 133 | assert isinstance(category["name"], str) 134 | 135 | 136 | @pytest.mark.asyncio 137 | async def test_invalid_account_id(): 138 | """Test error handling for invalid account ID.""" 139 | with pytest.raises(Exception): # Replace with specific exception when implemented 140 | await up_mcp.get_account("invalid-account-id") 141 | 142 | 143 | @pytest.mark.asyncio 144 | async def test_get_transaction(): 145 | """Test retrieving a specific transaction.""" 146 | # First get all transactions with verbose to get IDs 147 | transactions = await up_mcp.get_transactions(verbose=True) 148 | if len(transactions) > 0: 149 | transaction_id = transactions[0]["id"] 150 | 151 | # Get the specific transaction 152 | transaction = await up_mcp.get_transaction(transaction_id) 153 | assert transaction is not None 154 | 155 | # Check that the response is formatted properly 156 | # Actual response format is like: <Transaction HELD: -34.0 AUD [Claude Ai]> 157 | assert isinstance(transaction, str) 158 | assert transaction.startswith("<Transaction ") 159 | assert transaction.endswith(">") 160 | 161 | # Verify the transaction amount and description are present in the response 162 | tx_amount = str(abs(transactions[0]["amount"])) 163 | tx_desc = transactions[0]["description"] 164 | 165 | # The specific format might vary, but amount and description should be in the string 166 | assert tx_amount in transaction 167 | assert tx_desc in transaction 168 | 169 | 170 | @pytest.mark.asyncio 171 | async def test_categorize_transaction(): 172 | """Test categorizing a transaction.""" 173 | # First get all transactions with verbose to get IDs 174 | transactions = await up_mcp.get_transactions(verbose=True) 175 | if len(transactions) > 0: 176 | # Get categories to find a valid category ID 177 | categories = await up_mcp.get_categories() 178 | if len(categories) > 0: 179 | transaction_id = transactions[0]["id"] 180 | category_id = categories[0]["id"] 181 | 182 | # Attempt to categorize a transaction 183 | result = await up_mcp.categorize_transaction( 184 | transaction_id=transaction_id, 185 | category_id=category_id 186 | ) 187 | # Verify the result 188 | assert result is not None 189 | assert isinstance(result, bool) 190 | assert result == True 191 | 192 | 193 | @pytest.mark.asyncio 194 | async def test_webhooks(): 195 | """Test webhook operations.""" 196 | # Get existing webhooks 197 | webhooks = await up_mcp.get_webhooks() 198 | 199 | # Should at least return an empty list without error 200 | assert isinstance(webhooks, list) 201 | 202 | # Test creating a webhook 203 | test_url = "https://hello.requestcatcher.com/test" 204 | result = await up_mcp.create_webhook(url=test_url) 205 | 206 | # Verify webhook creation result 207 | assert result is not None 208 | assert isinstance(result, dict) 209 | assert "id" in result 210 | webhook_id = result["id"] 211 | 212 | # Test pinging the webhook 213 | ping_result = await up_mcp.ping_webhook(webhook_id=webhook_id) 214 | assert ping_result is not None 215 | 216 | # Test deleting the webhook 217 | delete_result = await up_mcp.delete_webhook(webhook_id=webhook_id) 218 | assert delete_result is not None 219 | 220 | # Verify webhook was deleted by checking the list again 221 | updated_webhooks = await up_mcp.get_webhooks() 222 | assert all(webhook["id"] != webhook_id for webhook in updated_webhooks) 223 | 224 | if __name__ == "__main__": 225 | pytest.main([__file__, "-v"]) ```