# Directory Structure ``` ├── .gitignore ├── .python-version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── pyproject.toml ├── README.md ├── server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.12 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # macOS system files .DS_Store .AppleDouble .LSOverride Icon ._* .Spotlight-V100 .Trashes .fseventsd .TemporaryItems .VolumeIcon.icns .com.apple.timemachine.donotpresent # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv # uv specific .uv/ .venv/ venv/ ENV/ .uuid/ # IDE specific files .idea/ .vscode/ *.swp *.swo *~ # Environment variables .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Raindrop MCP Server This is a Model Context Protocol (MCP) server for Raindrop.io powered by the [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk/tree/main). It provides an easy way to read and update your bookmarks from the Raindrop personal knowledge management system in simple, human language. This can be paired with the [Firecrawl MCP server](https://github.com/mendableai/firecrawl-mcp-server#) to read the URLs associated with your bookmarks and classify them accordingly. ## Requirements - Python 3.12+ - [uv](https://github.com/astral-sh/uv) package manager - [Claude Desktop](https://claude.ai/desktop) - A Raindrop.io account and API token ## Setup ### 1. Obtain a Raindrop API Token 1. Go to [Raindrop.io Developer Portal](https://app.raindrop.io/settings/integrations) 2. Create a new app 3. Copy your API token ### 2. Set Your API Token Set your Raindrop API token as an environment variable: 1. Create a .env file in the root directory 2. Add new line: ```RAINDROP_TOKEN="your_token_here"``` ## Development To run the server in development mode: ``` uv run mcp dev server.py ``` ## Installation To install the server to Claude Desktop: ``` uv run mcp install server.py ``` This will start the server locally and allow you to test changes. ## Features The server provides: - Access to your Raindrop collections and raindrop data through capabilities - Support for viewing root collections, child collections, or a specific collection by ID - Tools to create, update, and delete collections and raindrops - Tools to create and update new tags ## Example Queries After installing the server to Claude Desktop, you can ask Claude questions and commands like: - "Show me all my Raindrop collections" - "Do I have any collections related to programming?" - "Add this tag to all raindrops in this collection" - "Show me the details of my Raindrop collection with ID 12345" - "What child collections do I have in Raindrop?" - "Create a new Raindrop collection called 'Claude Resources'" Here is some example usage in Claude Desktop (paired with a Firecrawl MCP server): Input to Claude Desktop as the classificaiton system:  Output from Claude Desktop:  ## Tools The server provides the following MCP tools that let Claude Desktop perform actions on your Raindrop collections: ### create_collection Creates a new collection in Raindrop.io. **Parameters:** - `title` (required): Name of the collection - `view`: View type (list, grid, masonry, simple) - `public`: Whether the collection is public - `parent_id`: ID of parent collection (omit for root collection) ### update_collection Updates an existing collection in Raindrop.io. **Parameters:** - `collection_id` (required): ID of the collection to update - `title`: New name for the collection - `view`: View type (list, grid, masonry, simple) - `public`: Whether the collection is public - `parent_id`: ID of parent collection (omit for root collection) - `expanded`: Whether the collection is expanded ### delete_collection Deletes a collection from Raindrop.io. The raindrops will be moved to Trash. **Parameters:** - `collection_id` (required): ID of the collection to delete ### empty_trash Empties the trash in Raindrop.io, permanently deleting all raindrops in it. ### get_raindrop Gets a single raindrop from Raindrop.io by ID. **Parameters:** - `raindrop_id` (required): ID of the raindrop to fetch ### get_raindrops Gets multiple raindrops from a Raindrop.io collection. **Parameters:** - `collection_id` (required): ID of the collection to fetch raindrops from. Use 0 for all raindrops, -1 for unsorted, -99 for trash. - `search`: Optional search query - `sort`: Sorting order (options: -created, created, score, -sort, title, -title, domain, -domain) - `page`: Page number (starting from 0) - `perpage`: Items per page (max 50) - `nested`: Whether to include raindrops from nested collections ### get_tags Gets tags from Raindrop.io. **Parameters:** - `collection_id`: Optional ID of the collection to fetch tags from. When not specified, all tags from all collections will be retrieved. ### update_raindrop Updates an existing raindrop (bookmark) in Raindrop.io. **Parameters:** - `raindrop_id` (required): ID of the raindrop to update - `title`: New title for the raindrop - `excerpt`: New description/excerpt - `link`: New URL - `important`: Set to True to mark as favorite - `tags`: List of tags to assign - `collection_id`: ID of collection to move the raindrop to - `cover`: URL for the cover image - `type`: Type of the raindrop - `order`: Sort order (ascending) - set to 0 to move to first place - `pleaseParse`: Set to True to reparse metadata (cover, type) in the background ### update_many_raindrops Updates multiple raindrops at once within a collection. **Parameters:** - `collection_id` (required): ID of the collection containing raindrops to update - `ids`: Optional list of specific raindrop IDs to update - `important`: Set to True to mark as favorite, False to unmark - `tags`: List of tags to add (or empty list to remove all tags) - `cover`: URL for cover image (use '<screenshot>' to set screenshots for all) - `target_collection_id`: ID of collection to move raindrops to - `nested`: Include raindrops from nested collections - `search`: Optional search query to filter which raindrops to update ## Dependencies Please see `pyproject.toml` for dependancies. These will be installed automatically when using `uv run mcp install` or `uv run mcp dev`. ## Contributing Contributions are welcome! Here's how you can contribute to this project: 1. Fork the repository 2. Create a new branch (`git checkout -b feature/your-feature-name`) 3. Make your changes 4. Validate they work as intended 5. Commit your changes (`git commit -m 'Add some feature'`) 6. Push to the branch (`git push origin feature/your-feature-name`) 7. Open a pull request Please ensure your code follows the existing style and includes appropriate documentation. ## License This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details. ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown # Contributing to Raindrop MCP Server Thank you for your interest in contributing to the Raindrop MCP Server! Here's how you can help improve this project. ## How to Contribute 1. Fork the repository 2. Create a new branch (`git checkout -b feature/your-feature-name`) 3. Make your changes 4. Validate they work as intended 5. Commit your changes (`git commit -m 'Add some feature'`) 6. Push to the branch (`git push origin feature/your-feature-name`) 7. Open a pull request ## Pull Request Process 1. Ensure your code follows the existing style and includes appropriate documentation 2. Update the README.md with details of changes to the interface, if applicable 3. The version number will be updated according to [Semantic Versioning](http://semver.org/) 4. Your pull request will be merged once it has been reviewed and approved ## Code Standards - Follow PEP 8 style guidelines for Python code - Write meaningful commit messages - Include comments and docstrings for new functions and classes - Add tests for new functionality when possible ## Bug Reports and Feature Requests If you find a bug or have an idea for a new feature, please create an issue with: - A clear and descriptive title - A detailed description of the bug or feature - Steps to reproduce the issue (for bugs) - Any relevant screenshots or error messages ## Questions? If you have any questions about contributing, feel free to open an issue with your question. Thank you for contributing to make the Raindrop MCP Server better! ``` -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- ```markdown # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "raindrop-mcp" version = "0.1.0" description = "Raindrop.io MCP server for Claude Desktop" readme = "README.md" requires-python = ">=3.12" dependencies = [ "mcp[cli]>=1.6.0", "httpx>=0.24.0", "python-dotenv>=1.0.0", ] ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- ``` MIT License Copyright (c) 2025 ddaltn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- ```python import asyncio import json import os import logging import httpx from typing import Any, Dict, List, Optional from dotenv import load_dotenv from mcp.server.fastmcp import FastMCP # Load environment variables from .env file load_dotenv() # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("raindrop-mcp") # Constants API_URL = "https://api.raindrop.io/rest/v1" # Create FastMCP server mcp = FastMCP("Raindrop Collections API") # Helper function to get headers with authentication token async def get_headers(): # Get token from .env file token = os.getenv("RAINDROP_TOKEN") if not token: logger.warning("RAINDROP_TOKEN not found in .env file.") logger.warning("Please add your Raindrop API token to the .env file.") # Use proper Bearer token format for authorization return { "Authorization": f"Bearer {token}" if token else "", "Content-Type": "application/json" } # Define tools @mcp.tool("get_root_collections") async def get_root_collections() -> list: """ Get all root collections from Raindrop.io """ # Fetch collections from root endpoint headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: async with httpx.AsyncClient() as client: response = await client.get(f"{API_URL}/collections", headers=headers) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") data = response.json() if "items" not in data: raise ValueError("Unexpected API response format") # Format response - return native Python list return [ { "_id": c.get("_id", ""), "title": c.get("title", ""), "count": c.get("count", 0), "public": c.get("public", False), "view": c.get("view", ""), "color": c.get("color", ""), "created": c.get("created", ""), "lastUpdate": c.get("lastUpdate", ""), "expanded": c.get("expanded", False) } for c in data["items"] ] except Exception as e: raise ValueError(f"Error fetching root collections: {str(e)}") @mcp.tool("get_child_collections") async def get_child_collections() -> str: """ Get all child collections from Raindrop.io """ headers = await get_headers() if not headers.get("Authorization"): return json.dumps({ "isError": True, "content": [{"type": "text", "text": "API token not set. Please check your .env file."}] }) try: async with httpx.AsyncClient() as client: response = await client.get(f"{API_URL}/collections/childrens", headers=headers) if response.status_code != 200: return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error: API returned status {response.status_code}"}] }) data = response.json() if "items" not in data: return json.dumps({ "isError": True, "content": [{"type": "text", "text": "Error: Unexpected API response format"}] }) # Format response result = [ { "_id": c.get("_id", ""), "title": c.get("title", ""), "count": c.get("count", 0), "public": c.get("public", False), "view": c.get("view", ""), "color": c.get("color", ""), "parent_id": c.get("parent", {}).get("$id", None), "created": c.get("created", ""), "lastUpdate": c.get("lastUpdate", ""), "expanded": c.get("expanded", False) } for c in data["items"] ] return json.dumps({ "content": [ {"type": "text", "text": f"Found {len(result)} child collection(s)"}, {"type": "json", "json": result} ] }) except Exception as e: return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error: {str(e)}"}] }) @mcp.tool("get_collection_by_id") async def get_collection_by_id(collection_id: int) -> dict: """ Get a specific collection from Raindrop.io by ID Args: collection_id: ID of the collection to fetch """ # Validate input if collection_id is None: raise ValueError("No collection ID provided") # Fetch specific collection by ID headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: async with httpx.AsyncClient() as client: response = await client.get(f"{API_URL}/collection/{collection_id}", headers=headers) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") data = response.json() if "item" not in data: raise ValueError("Unexpected API response format") # Format the single collection response collection = data["item"] result = { "_id": collection.get("_id", ""), "title": collection.get("title", ""), "count": collection.get("count", 0), "public": collection.get("public", False), "view": collection.get("view", ""), "color": collection.get("color", ""), "created": collection.get("created", ""), "lastUpdate": collection.get("lastUpdate", ""), "expanded": collection.get("expanded", False) } # Add parent ID if present if "parent" in collection and "$id" in collection["parent"]: result["parent_id"] = collection["parent"]["$id"] # Return native Python dict instead of JSON string return result except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error fetching collection: {str(e)}") @mcp.tool("create_collection") async def create_collection( title: str, view: str = "list", public: bool = False, parent_id: Optional[int] = None ) -> str: """ Create a new collection in Raindrop.io Args: title: Name of the collection view: View type (list, grid, masonry, simple) public: Whether the collection is public parent_id: ID of parent collection (omit for root collection) """ headers = await get_headers() if not headers.get("Authorization"): return json.dumps({ "isError": True, "content": [{"type": "text", "text": "API token not set. Please check your .env file."}] }) try: data = { "title": title, "view": view, "public": public } if parent_id is not None: data["parent"] = {"$id": parent_id} async with httpx.AsyncClient() as client: response = await client.post( f"{API_URL}/collection", headers=headers, json=data ) response_data = response.json() if not response_data.get("result", False): return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error creating collection: {response_data.get('errorMessage', 'Unknown error')}"}] }) return json.dumps({ "content": [ {"type": "text", "text": f"Collection '{title}' created successfully."}, {"type": "text", "text": f"Collection ID: {response_data.get('item', {}).get('_id', 'Unknown')}"} ] }) except Exception as e: raise e # Let exceptions bubble up @mcp.tool("update_collection") async def update_collection( collection_id: int, title: Optional[str] = None, view: Optional[str] = None, public: Optional[bool] = None, parent_id: Optional[int] = None, expanded: Optional[bool] = None ) -> str: """ Update an existing collection in Raindrop.io Args: collection_id: ID of the collection to update title: New name for the collection view: View type (list, grid, masonry, simple) public: Whether the collection is public parent_id: ID of parent collection (omit for root collection) expanded: Whether the collection is expanded """ headers = await get_headers() if not headers.get("Authorization"): return json.dumps({ "isError": True, "content": [{"type": "text", "text": "API token not set. Please check your .env file."}] }) try: data = {} if title is not None: data["title"] = title if view is not None: data["view"] = view if public is not None: data["public"] = public if parent_id is not None: data["parent"] = {"$id": parent_id} if expanded is not None: data["expanded"] = expanded if not data: return json.dumps({ "isError": True, "content": [{"type": "text", "text": "No update parameters provided."}] }) async with httpx.AsyncClient() as client: response = await client.put( f"{API_URL}/collection/{collection_id}", headers=headers, json=data ) response_data = response.json() if not response_data.get("result", False): return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error updating collection: {response_data.get('errorMessage', 'Unknown error')}"}] }) return json.dumps({ "content": [ {"type": "text", "text": f"Collection {collection_id} updated successfully."} ] }) except Exception as e: raise e # Let exceptions bubble up @mcp.tool("delete_collection") async def delete_collection(collection_id: int) -> str: """ Delete a collection from Raindrop.io. The raindrops will be moved to Trash. Args: collection_id: ID of the collection to delete """ headers = await get_headers() if not headers.get("Authorization"): return json.dumps({ "isError": True, "content": [{"type": "text", "text": "API token not set. Please check your .env file."}] }) try: async with httpx.AsyncClient() as client: response = await client.delete( f"{API_URL}/collection/{collection_id}", headers=headers ) response_data = response.json() if not response_data.get("result", False): return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error deleting collection: {response_data.get('errorMessage', 'Unknown error')}"}] }) return json.dumps({ "content": [ {"type": "text", "text": f"Collection {collection_id} deleted successfully."} ] }) except Exception as e: raise e # Let exceptions bubble up @mcp.tool("empty_trash") async def empty_trash() -> str: """ Empty the trash in Raindrop.io, permanently deleting all raindrops in it. """ headers = await get_headers() if not headers.get("Authorization"): return json.dumps({ "isError": True, "content": [{"type": "text", "text": "API token not set. Please check your .env file."}] }) try: async with httpx.AsyncClient() as client: response = await client.delete( f"{API_URL}/collection/-99", headers=headers ) response_data = response.json() if not response_data.get("result", False): return json.dumps({ "isError": True, "content": [{"type": "text", "text": f"Error emptying trash: {response_data.get('errorMessage', 'Unknown error')}"}] }) return json.dumps({ "content": [ {"type": "text", "text": "Trash emptied successfully."} ] }) except Exception as e: raise e # Let exceptions bubble up @mcp.tool("get_raindrop") async def get_raindrop(raindrop_id: int) -> dict: """ Get a single raindrop from Raindrop.io by ID Args: raindrop_id: ID of the raindrop to fetch """ # Validate input if raindrop_id is None: raise ValueError("No raindrop ID provided") headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: async with httpx.AsyncClient() as client: response = await client.get(f"{API_URL}/raindrop/{raindrop_id}", headers=headers) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") data = response.json() if "item" not in data: raise ValueError("Unexpected API response format") # Return the raindrop item directly return data["item"] except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error fetching raindrop: {str(e)}") @mcp.tool("get_raindrops") async def get_raindrops( collection_id: int, search: Optional[str] = None, sort: Optional[str] = None, page: Optional[int] = 0, perpage: Optional[int] = 25, nested: Optional[bool] = False ) -> dict: """ Get multiple raindrops from a Raindrop.io collection Args: collection_id: ID of the collection to fetch raindrops from. Use 0 for all raindrops, -1 for unsorted, -99 for trash. search: Optional search query sort: Sorting order. Options: -created (default), created, score, -sort, title, -title, domain, -domain page: Page number (starting from 0) perpage: Items per page (max 50) nested: Whether to include raindrops from nested collections """ # Validate inputs if collection_id is None: raise ValueError("No collection ID provided") if perpage > 50: perpage = 50 # API limit headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: # Build query parameters params = {} if search: params["search"] = search if sort: params["sort"] = sort if page is not None: params["page"] = page if perpage: params["perpage"] = perpage if nested: params["nested"] = "true" async with httpx.AsyncClient() as client: response = await client.get( f"{API_URL}/raindrops/{collection_id}", headers=headers, params=params ) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") return response.json() except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error fetching raindrops: {str(e)}") @mcp.tool("get_tags") async def get_tags(collection_id: Optional[int] = None) -> list: """ Get tags from Raindrop.io Args: collection_id: Optional ID of the collection to fetch tags from. When not specified, all tags from all collections will be retrieved. """ headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: # Build the endpoint URL endpoint = f"{API_URL}/tags" if collection_id is not None: endpoint = f"{endpoint}/{collection_id}" async with httpx.AsyncClient() as client: response = await client.get(endpoint, headers=headers) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") data = response.json() if not data.get("result", False) or "items" not in data: raise ValueError("Unexpected API response format") # Return just the tags array for simplicity return data["items"] except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error fetching tags: {str(e)}") @mcp.tool("update_raindrop") async def update_raindrop( raindrop_id: int, title: Optional[str] = None, excerpt: Optional[str] = None, link: Optional[str] = None, important: Optional[bool] = None, tags: Optional[List[str]] = None, collection_id: Optional[int] = None, cover: Optional[str] = None, type: Optional[str] = None, order: Optional[int] = None, pleaseParse: Optional[bool] = None ) -> dict: """ Update an existing raindrop (bookmark) in Raindrop.io Args: raindrop_id: ID of the raindrop to update title: New title for the raindrop excerpt: New description/excerpt link: New URL important: Set to True to mark as favorite tags: List of tags to assign collection_id: ID of collection to move the raindrop to cover: URL for the cover image type: Type of the raindrop order: Sort order (ascending) - set to 0 to move to first place pleaseParse: Set to True to reparse metadata (cover, type) in the background """ # Validate input if raindrop_id is None: raise ValueError("No raindrop ID provided") headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: # Build the request body with only provided parameters data = {} if title is not None: data["title"] = title if excerpt is not None: data["excerpt"] = excerpt if link is not None: data["link"] = link if important is not None: data["important"] = important if tags is not None: data["tags"] = tags if collection_id is not None: data["collection"] = {"$id": collection_id} if cover is not None: data["cover"] = cover if type is not None: data["type"] = type if order is not None: data["order"] = order if pleaseParse: data["pleaseParse"] = {} if not data: raise ValueError("No update parameters provided") async with httpx.AsyncClient() as client: response = await client.put( f"{API_URL}/raindrop/{raindrop_id}", headers=headers, json=data ) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") result = response.json() if not result.get("result", False): error_message = result.get("errorMessage", "Unknown error") raise ValueError(f"Error updating raindrop: {error_message}") return result except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error updating raindrop: {str(e)}") @mcp.tool("update_many_raindrops") async def update_many_raindrops( collection_id: int, ids: Optional[List[int]] = None, important: Optional[bool] = None, tags: Optional[List[str]] = None, cover: Optional[str] = None, target_collection_id: Optional[int] = None, nested: Optional[bool] = False, search: Optional[str] = None ) -> dict: """ Update multiple raindrops at once within a collection Args: collection_id: ID of the collection containing raindrops to update ids: Optional list of specific raindrop IDs to update important: Set to True to mark as favorite, False to unmark tags: List of tags to add (or empty list to remove all tags) cover: URL for cover image (use '<screenshot>' to set screenshots for all) target_collection_id: ID of collection to move raindrops to nested: Include raindrops from nested collections search: Optional search query to filter which raindrops to update """ # Validate input if collection_id is None: raise ValueError("No collection ID provided") headers = await get_headers() if not headers.get("Authorization"): raise ValueError("API token not set. Please check your .env file.") try: # Build the request body with only provided parameters data = {} if ids is not None: data["ids"] = ids if important is not None: data["important"] = important if tags is not None: data["tags"] = tags if cover is not None: data["cover"] = cover if target_collection_id is not None: data["collection"] = {"$id": target_collection_id} if not data: raise ValueError("No update parameters provided") # Build query params params = {} if search: params["search"] = search if nested: params["nested"] = "true" async with httpx.AsyncClient() as client: response = await client.put( f"{API_URL}/raindrops/{collection_id}", headers=headers, json=data, params=params ) if response.status_code != 200: raise ValueError(f"API returned status {response.status_code}") result = response.json() if not result.get("result", False): error_message = result.get("errorMessage", "Unknown error") raise ValueError(f"Error updating raindrops: {error_message}") return result except Exception as e: # Let exceptions bubble up to MCP raise ValueError(f"Error updating raindrops: {str(e)}") if __name__ == "__main__": asyncio.run(mcp.run()) ```