# Directory Structure ``` ├── .env.example ├── .gitignore ├── .python-version ├── pyproject.toml ├── README.md ├── server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.11 2 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | MOCHI_API_KEY="" 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .env 12 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mochi-cards" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.5.0", 10 | ] 11 | ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | from typing import Any 3 | import httpx 4 | from mcp.server.fastmcp import FastMCP 5 | 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | mcp = FastMCP("mochi-cards") 11 | 12 | MOCHI_API_BASE = "https://app.mochi.cards/api" 13 | MOCHI_API_KEY = os.getenv("MOCHI_API_KEY") 14 | 15 | 16 | async def make_mochi_request( 17 | url: str, method: str = "GET", data: dict = None 18 | ) -> dict[str, Any] | None: 19 | """Make a request to the Mochi API with proper error handling.""" 20 | headers = {"Accept": "application/json", "Content-Type": "application/json"} 21 | auth = (MOCHI_API_KEY, "") if MOCHI_API_KEY else None 22 | 23 | async with httpx.AsyncClient() as client: 24 | try: 25 | if method == "GET": 26 | response = await client.get( 27 | url, headers=headers, auth=auth, timeout=30.0 28 | ) 29 | elif method == "POST": 30 | response = await client.post( 31 | url, headers=headers, json=data, auth=auth, timeout=30.0 32 | ) 33 | elif method == "DELETE": 34 | response = await client.delete( 35 | url, headers=headers, auth=auth, timeout=30.0 36 | ) 37 | 38 | response.raise_for_status() 39 | return response.json() if response.content else None 40 | except Exception as e: 41 | return {"error": str(e)} 42 | 43 | 44 | @mcp.tool() 45 | async def mochi_list_decks(bookmark: str = None) -> str: 46 | """List all decks in your Mochi account. 47 | 48 | Args: 49 | api_key: Your Mochi API key 50 | bookmark: Optional bookmark for pagination 51 | """ 52 | url = f"{MOCHI_API_BASE}/decks" 53 | if bookmark: 54 | url += f"?bookmark={bookmark}" 55 | 56 | response = await make_mochi_request(url) 57 | 58 | if not response or "error" in response: 59 | return f"Error fetching decks: {response.get('error', 'Unknown error')}" 60 | 61 | decks = response.get("docs", []) 62 | result = "Your Mochi Decks:\n\n" 63 | 64 | for deck in decks: 65 | result += f"ID: {deck.get('id')}\nName: {deck.get('name')}\n\n" 66 | 67 | if response.get("bookmark"): 68 | result += f"\nMore decks available. Use bookmark: {response.get('bookmark')}" 69 | 70 | return result 71 | 72 | 73 | @mcp.tool() 74 | async def mochi_create_card(deck_id: str, content: str, tags: list = None) -> str: 75 | """Create a new card in a Mochi deck. 76 | 77 | Args: 78 | api_key: Your Mochi API key 79 | deck_id: The ID of the deck to add the card to 80 | content: Markdown content for the card 81 | tags: Optional list of tags to add to the card 82 | """ 83 | url = f"{MOCHI_API_BASE}/cards" 84 | 85 | data = {"content": content, "deck-id": deck_id} 86 | 87 | if tags: 88 | data["manual-tags"] = tags 89 | 90 | response = await make_mochi_request(url, method="POST", data=data) 91 | 92 | if not response or "error" in response: 93 | return f"Error creating card: {response.get('error', 'Unknown error')}" 94 | 95 | return f"Card created successfully with ID: {response.get('id')}" 96 | 97 | 98 | @mcp.tool() 99 | async def mochi_get_card(card_id: str) -> str: 100 | """Get details of a specific Mochi card. 101 | 102 | Args: 103 | api_key: Your Mochi API key 104 | card_id: The ID of the card to retrieve 105 | """ 106 | url = f"{MOCHI_API_BASE}/cards/{card_id}" 107 | 108 | response = await make_mochi_request(url) 109 | 110 | if not response or "error" in response: 111 | return f"Error fetching card: {response.get('error', 'Unknown error')}" 112 | 113 | result = f"Card ID: {response.get('id')}\n" 114 | result += f"Deck ID: {response.get('deck-id')}\n" 115 | result += f"Content: {response.get('content')}\n" 116 | result += f"Created: {response.get('created-at', {}).get('date')}\n" 117 | result += f"Updated: {response.get('updated-at', {}).get('date')}\n" 118 | 119 | if response.get("tags"): 120 | result += f"Tags: {', '.join(response.get('tags'))}\n" 121 | 122 | return result 123 | 124 | 125 | @mcp.tool() 126 | async def mochi_update_card( 127 | card_id: str, 128 | content: str = None, 129 | deck_id: str = None, 130 | archived: bool = None, 131 | tags: list = None, 132 | ) -> str: 133 | """Update an existing Mochi card. 134 | 135 | Args: 136 | api_key: Your Mochi API key 137 | card_id: The ID of the card to update 138 | content: Optional new markdown content 139 | deck_id: Optional new deck ID to move the card to 140 | archived: Optional boolean to archive/unarchive the card 141 | tags: Optional list of tags to replace existing tags 142 | """ 143 | url = f"{MOCHI_API_BASE}/cards/{card_id}" 144 | 145 | data = {} 146 | if content is not None: 147 | data["content"] = content 148 | if deck_id is not None: 149 | data["deck-id"] = deck_id 150 | if archived is not None: 151 | data["archived?"] = archived 152 | if tags is not None: 153 | data["manual-tags"] = tags 154 | 155 | response = await make_mochi_request( 156 | url, 157 | method="POST", 158 | data=data, 159 | ) 160 | 161 | if not response or "error" in response: 162 | return f"Error updating card: {response.get('error', 'Unknown error')}" 163 | 164 | return f"Card {card_id} updated successfully" 165 | 166 | 167 | @mcp.tool() 168 | async def mochi_delete_card(card_id: str) -> str: 169 | """Delete a Mochi card permanently. 170 | 171 | Args: 172 | api_key: Your Mochi API key 173 | card_id: The ID of the card to delete 174 | """ 175 | url = f"{MOCHI_API_BASE}/cards/{card_id}" 176 | 177 | response = await make_mochi_request(url, method="DELETE") 178 | 179 | if response and "error" in response: 180 | return f"Error deleting card: {response.get('error')}" 181 | 182 | return f"Card {card_id} deleted successfully" 183 | 184 | 185 | if __name__ == "__main__": 186 | mcp.run(transport="stdio") 187 | ```