#
tokens: 7869/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
![classifier](https://github.com/user-attachments/assets/648d587f-6e10-42b3-b759-878110ce1d66)

Output from Claude Desktop:
![classifier-output](https://github.com/user-attachments/assets/60d67757-cda5-472b-895d-c31b1fdd3631)


## 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()) 
```