├── .gitignore ├── LICENSE ├── README.md ├── cursor-db-mcp-server.py ├── img ├── claude-logo.png ├── cursor-db-keys.png ├── cursor-db-mcp-claude.gif ├── cursor-db-mcp-in-cursor.gif ├── cursor-db-mcp.png ├── cursor-db-structure.png ├── cursor-journal-logo_thumbnail.jpg └── mcp-cursor-db-search.png ├── install.py ├── requirements.txt └── test_mcp_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | .venv/ 8 | venv/ 9 | ENV/ 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Cursor specific 28 | tmp/ 29 | 30 | # Logs 31 | *.log 32 | 33 | # OS specific 34 | .DS_Store 35 | .DS_Store? 36 | ._* 37 | .Spotlight-V100 38 | .Trashes 39 | ehthumbs.db 40 | Thumbs.db 41 | 42 | # IDE specific 43 | .idea/ 44 | .vscode/ 45 | *.swp 46 | *.swo 47 | 48 | # Project test results 49 | test-results.txt 50 | 51 | .cursor 52 | mcp-server.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2025 John Damask. 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cursor DB MCP Server 2 | 3 | A Model Context Protocol (MCP) server for accessing Cursor IDE's SQLite databases. This server allows AI assistants to explore and interact with Cursor's project data, chat history, and composer information. 4 | 5 | 7 | 8 | __Cursor__ 9 | ![In Cursor GIF](./img/cursor-db-mcp-in-cursor.gif) 10 | 11 | 12 | ## Prerequisites 13 | 14 | Cursor IDE 15 | 16 | 17 | ## Installation 18 | 19 | ### Easy Installation 20 | 21 | Use the provided installation script to install all dependencies: 22 | 23 | ```bash 24 | python install.py 25 | ``` 26 | 27 | This script will install: 28 | - Basic MCP server and dependencies 29 | 30 | 52 | 53 | 83 | 84 | ## Using with Cursor IDE 85 | 86 | 1. Open Cursor and navigate to Settings->Cursor Settings->MCP. 87 | 2. Click: Add new MCP server 88 | 3. Name: Cursor DB MCP; Type: Command 89 | 4. Command: \uv run --with mcp[cli] mcp run \/cursor-db-mcp-server.py 90 | 91 | ![Cursor DB MCP](./img/cursor-db-mcp.png) 92 | 93 | Now you can ask questions about the database or retrieve info about historical chats. 94 | 95 | ![DB structure](./img/cursor-db-structure.png) 96 | 97 | ![DB keys](./img/cursor-db-keys.png) 98 | 99 | ### Using with Claude Desktop 100 | 101 | [Installing MCP servers for Claude Desktop](https://modelcontextprotocol.io/quickstart/user) 102 | 103 | Add this to your claude_desktop_config.json file 104 | ``` 105 | "cursor-db-mcp": { 106 | "command": "/uv", 107 | "args": [ 108 | "run", 109 | "--with", 110 | "mcp[cli]", 111 | "mcp", 112 | "run", 113 | "/cursor-db-mcp-server.py" 114 | ] 115 | } 116 | ``` 117 | 118 | 119 | ![Cursor DB fuzzy search](./img/mcp-cursor-db-search.png) 120 | 121 | ## Available Resources 122 | 123 | - `cursor://projects` - List all available Cursor projects 124 | - `cursor://projects/detailed` - List projects with detailed information 125 | - `cursor://projects/{project_name}/chat` - Get chat data for a specific project 126 | - `cursor://projects/{project_name}/composers` - Get composer IDs for a specific project 127 | - `cursor://composers/{composer_id}` - Get data for a specific composer 128 | 129 | ## Available Tools 130 | 131 | - `query_table` - Query a specific table in a project's database 132 | - `refresh_databases` - Refresh the list of database paths 133 | 134 | 135 | 220 | 221 | # How It Works 222 | 223 | The server scans your Cursor installation directory to find project databases (state.vscdb files). It then exposes these databases through MCP resources and tools, allowing AI assistants to query and analyze the data. 224 | 225 | # Notes 226 | 1. Cursor stores AI conversations in different places. Increasingly, chats are stored as "composerData" under globalStorage/state.vscdb. If you don't get results when asking about chats for recent projects, try asking for composers. 227 | 2. This was written on a Mac. YMMV with other OS 228 | 229 | # Shameless Plug 230 | 231 | 232 | Like this? Try [Cursor Journal](https://medium.com/@jbdamask/building-cursor-journal-with-cursor-77445026a08c) to create DevLogs directly from Cursor chat history! 233 | 234 | # License 235 | 236 | MIT -------------------------------------------------------------------------------- /cursor-db-mcp-server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import sqlite3 4 | import platform 5 | import re 6 | from pathlib import Path 7 | import argparse 8 | import logging 9 | from typing import Dict, List, Optional, Any, Union, AsyncIterator 10 | from contextlib import asynccontextmanager 11 | import sys 12 | 13 | # Configure logging 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 17 | handlers=[ 18 | logging.FileHandler(os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp-server.log")), 19 | logging.StreamHandler() 20 | ] 21 | ) 22 | logger = logging.getLogger('cursor-mcp') 23 | 24 | # Import MCP libraries 25 | try: 26 | from mcp.server.fastmcp import FastMCP, Context 27 | except ImportError as e: 28 | logger.error(f"Failed to import MCP libraries: {str(e)}. Make sure they are installed.") 29 | sys.exit(1) 30 | 31 | # Global DB manager instance 32 | db_manager = None 33 | 34 | class CursorDBManager: 35 | def __init__(self, cursor_path=None, project_dirs=None): 36 | """ 37 | Initialize the CursorDBManager with a Cursor main directory and/or list of project directories. 38 | 39 | Args: 40 | cursor_path (str): Path to main Cursor directory (e.g. ~/Library/Application Support/Cursor/User/) 41 | project_dirs (list): List of paths to Cursor project directories containing state.vscdb files 42 | """ 43 | if cursor_path: 44 | self.cursor_path = Path(cursor_path).expanduser().resolve() 45 | else: 46 | # Try to get the default cursor path 47 | self.cursor_path = self.get_default_cursor_path() 48 | 49 | self.project_dirs = project_dirs or [] 50 | self.db_paths = {} 51 | self.projects_info = {} 52 | self.global_db_path = None 53 | self.refresh_db_paths() 54 | 55 | def get_default_cursor_path(self): 56 | """Return the default Cursor path based on the operating system""" 57 | system = platform.system() 58 | home = Path.home() 59 | 60 | default_path = None 61 | if system == "Darwin": # macOS 62 | default_path = home / "Library/Application Support/Cursor/User" 63 | elif system == "Windows": 64 | default_path = home / "AppData/Roaming/Cursor/User" 65 | elif system == "Linux": 66 | default_path = home / ".config/Cursor/User" 67 | else: 68 | logger.warning(f"Unknown operating system: {system}. Cannot determine default Cursor path.") 69 | return None 70 | 71 | logger.info(f"Detected default Cursor path for {system}: {default_path}") 72 | return default_path 73 | 74 | def detect_cursor_projects(self): 75 | """Detect Cursor projects by scanning the workspaceStorage directory""" 76 | if not self.cursor_path: 77 | logger.error("No Cursor path available") 78 | return [] 79 | 80 | # Check if the path exists 81 | if not self.cursor_path.exists(): 82 | logger.error(f"Cursor path does not exist: {self.cursor_path}") 83 | return [] 84 | 85 | workspace_storage = self.cursor_path / "workspaceStorage" 86 | if not workspace_storage.exists(): 87 | logger.warning(f"Workspace storage directory not found: {workspace_storage}") 88 | return [] 89 | 90 | logger.info(f"Found workspace storage directory: {workspace_storage}") 91 | 92 | projects = [] 93 | 94 | # Scan all subdirectories in workspaceStorage 95 | for workspace_dir in workspace_storage.iterdir(): 96 | if not workspace_dir.is_dir(): 97 | continue 98 | 99 | workspace_json = workspace_dir / "workspace.json" 100 | state_db = workspace_dir / "state.vscdb" 101 | 102 | if workspace_json.exists() and state_db.exists(): 103 | try: 104 | with open(workspace_json, 'r') as f: 105 | workspace_data = json.load(f) 106 | 107 | folder_uri = workspace_data.get("folder") 108 | if folder_uri: 109 | # Extract the project name from the URI 110 | # For "file:///Users/johndamask/code/cursor-chat-browser", get "cursor-chat-browser" 111 | project_name = folder_uri.rstrip('/').split('/')[-1] 112 | 113 | projects.append({ 114 | "name": project_name, 115 | "db_path": str(state_db), 116 | "workspace_dir": str(workspace_dir), 117 | "folder_uri": folder_uri 118 | }) 119 | logger.info(f"Found project: {project_name} at {state_db}") 120 | except Exception as e: 121 | logger.error(f"Error processing workspace: {workspace_dir}: {e}") 122 | 123 | return projects 124 | 125 | def refresh_db_paths(self): 126 | """Scan project directories and identify all state.vscdb files""" 127 | self.db_paths = {} 128 | self.projects_info = {} 129 | 130 | # First, detect projects from the Cursor directory 131 | if self.cursor_path: 132 | cursor_projects = self.detect_cursor_projects() 133 | for project in cursor_projects: 134 | project_name = project["name"] 135 | self.db_paths[project_name] = project["db_path"] 136 | self.projects_info[project_name] = project 137 | 138 | # Set the global storage database path 139 | global_storage_path = self.cursor_path / "globalStorage" / "state.vscdb" 140 | if global_storage_path.exists(): 141 | self.global_db_path = str(global_storage_path) 142 | logger.info(f"Found global storage database at {self.global_db_path}") 143 | else: 144 | logger.warning(f"Global storage database not found at {global_storage_path}") 145 | 146 | # Then add explicitly specified project directories 147 | for project_dir in self.project_dirs: 148 | project_path = Path(project_dir).expanduser().resolve() 149 | db_path = project_path / "state.vscdb" 150 | 151 | if db_path.exists(): 152 | project_name = project_path.name 153 | self.db_paths[project_name] = str(db_path) 154 | self.projects_info[project_name] = { 155 | "name": project_name, 156 | "db_path": str(db_path), 157 | "workspace_dir": None, 158 | "folder_uri": None 159 | } 160 | logger.info(f"Found database: {project_name} at {db_path}") 161 | else: 162 | logger.warning(f"No state.vscdb found in {project_path}") 163 | 164 | # def add_project_dir(self, project_dir): 165 | # """Add a new project directory to the manager""" 166 | # project_path = Path(project_dir).expanduser().resolve() 167 | # if project_path not in self.project_dirs: 168 | # self.project_dirs.append(project_path) 169 | # self.refresh_db_paths() 170 | # return len(self.db_paths) 171 | 172 | def list_projects(self, detailed=False): 173 | """ 174 | Return list of available projects 175 | 176 | Args: 177 | detailed (bool): Whether to return detailed project information 178 | 179 | Returns: 180 | dict: Project information (either just DB paths or full details) 181 | """ 182 | if detailed: 183 | return self.projects_info 184 | return self.db_paths 185 | 186 | def execute_query(self, project_name, table_name, query_type, key=None, limit=100): 187 | """ 188 | Execute a query against a specific project's database 189 | 190 | Args: 191 | project_name (str): Name of the project (key in db_paths) 192 | table_name (str): Either 'ItemTable' or 'cursorDiskKV' 193 | query_type (str): Type of query ('get_all', 'get_by_key', 'search_keys') 194 | key (str, optional): Key to search for when using 'get_by_key' or 'search_keys' 195 | limit (int): Maximum number of results to return 196 | 197 | Returns: 198 | list: Query results 199 | """ 200 | if project_name not in self.db_paths: 201 | raise ValueError(f"Project '{project_name}' not found") 202 | 203 | if table_name not in ["ItemTable", "cursorDiskKV"]: 204 | raise ValueError("Table name must be either 'ItemTable' or 'cursorDiskKV'") 205 | 206 | db_path = self.db_paths[project_name] 207 | 208 | try: 209 | conn = sqlite3.connect(db_path) 210 | cursor = conn.cursor() 211 | 212 | if query_type == "get_all": 213 | cursor.execute(f"SELECT key, value FROM {table_name} LIMIT ?", (limit,)) 214 | elif query_type == "get_by_key" and key: 215 | cursor.execute(f"SELECT key, value FROM {table_name} WHERE key = ?", (key,)) 216 | elif query_type == "search_keys" and key: 217 | search_term = f"%{key}%" 218 | cursor.execute(f"SELECT key, value FROM {table_name} WHERE key LIKE ? LIMIT ?", 219 | (search_term, limit)) 220 | else: 221 | raise ValueError("Invalid query type or missing key parameter") 222 | 223 | results = [] 224 | for row in cursor.fetchall(): 225 | key, value = row 226 | try: 227 | # Try to parse JSON value 228 | parsed_value = json.loads(value) 229 | results.append({"key": key, "value": parsed_value}) 230 | except json.JSONDecodeError: 231 | # If not valid JSON, return as string 232 | results.append({"key": key, "value": value}) 233 | 234 | conn.close() 235 | return results 236 | 237 | except sqlite3.Error as e: 238 | logger.error(f"SQLite error: {e}") 239 | raise 240 | 241 | def get_chat_data(self, project_name): 242 | """ 243 | Retrieve AI chat data from a project 244 | 245 | Args: 246 | project_name (str): Name of the project 247 | 248 | Returns: 249 | dict: Chat data from the project 250 | """ 251 | if project_name not in self.db_paths: 252 | raise ValueError(f"Project '{project_name}' not found") 253 | 254 | try: 255 | results = self.execute_query( 256 | project_name, 257 | "ItemTable", 258 | "get_by_key", 259 | "workbench.panel.aichat.view.aichat.chatdata" 260 | ) 261 | 262 | if results and len(results) > 0: 263 | return results[0]["value"] 264 | else: 265 | return {"error": "No chat data found for this project"} 266 | 267 | except Exception as e: 268 | logger.error(f"Error retrieving chat data: {e}") 269 | raise 270 | 271 | def get_composer_ids(self, project_name): 272 | """ 273 | Retrieve composer IDs from a project 274 | 275 | Args: 276 | project_name (str): Name of the project 277 | 278 | Returns: 279 | list: List of composer IDs 280 | """ 281 | if project_name not in self.db_paths: 282 | raise ValueError(f"Project '{project_name}' not found") 283 | 284 | try: 285 | results = self.execute_query( 286 | project_name, 287 | "ItemTable", 288 | "get_by_key", 289 | "composer.composerData" 290 | ) 291 | 292 | if results and len(results) > 0: 293 | composer_data = results[0]["value"] 294 | # Extract composer IDs from the data 295 | composer_ids = [] 296 | if "allComposers" in composer_data: 297 | for composer in composer_data["allComposers"]: 298 | if "composerId" in composer: 299 | composer_ids.append(composer["composerId"]) 300 | return { 301 | "composer_ids": composer_ids, 302 | "full_data": composer_data 303 | } 304 | else: 305 | return {"error": "No composer data found for this project"} 306 | 307 | except Exception as e: 308 | logger.error(f"Error retrieving composer IDs: {e}") 309 | raise 310 | 311 | def get_composer_data(self, composer_id): 312 | """ 313 | Retrieve composer data from global storage 314 | 315 | Args: 316 | composer_id (str): Composer ID 317 | 318 | Returns: 319 | dict: Composer data 320 | """ 321 | if not self.global_db_path: 322 | raise ValueError("Global storage database not found") 323 | 324 | try: 325 | conn = sqlite3.connect(self.global_db_path) 326 | cursor = conn.cursor() 327 | 328 | key = f"composerData:{composer_id}" 329 | cursor.execute("SELECT value FROM cursorDiskKV WHERE key = ?", (key,)) 330 | 331 | row = cursor.fetchone() 332 | conn.close() 333 | 334 | if row: 335 | try: 336 | return {"composer_id": composer_id, "data": json.loads(row[0])} 337 | except json.JSONDecodeError: 338 | return {"composer_id": composer_id, "data": row[0]} 339 | else: 340 | return {"error": f"No data found for composer ID: {composer_id}"} 341 | 342 | except sqlite3.Error as e: 343 | logger.error(f"SQLite error: {e}") 344 | raise 345 | 346 | # Create an MCP server with lifespan support 347 | @asynccontextmanager 348 | async def app_lifespan(app: FastMCP) -> AsyncIterator[Dict[str, Any]]: 349 | """Manage application lifecycle with context""" 350 | try: 351 | # Initialize the DB manager on startup 352 | global db_manager 353 | db_manager = CursorDBManager() 354 | 355 | # Parse command line arguments 356 | parser = argparse.ArgumentParser(description='Cursor IDE SQLite Database MCP Server') 357 | parser.add_argument('--cursor-path', help='Path to Cursor User directory (e.g. ~/Library/Application Support/Cursor/User/)') 358 | parser.add_argument('--project-dirs', nargs='+', help='List of additional Cursor project directories to scan') 359 | 360 | # Parse known args only, to avoid conflicts with MCP's own args 361 | args, _ = parser.parse_known_args() 362 | 363 | # Configure the DB manager with the Cursor path 364 | if args.cursor_path: 365 | db_manager.cursor_path = Path(args.cursor_path).expanduser().resolve() 366 | 367 | # Add explicitly specified project directories 368 | if args.project_dirs: 369 | for project_dir in args.project_dirs: 370 | db_manager.add_project_dir(project_dir) 371 | 372 | # Log detected Cursor path 373 | if db_manager.cursor_path: 374 | logger.info(f"Using Cursor path: {db_manager.cursor_path}") 375 | else: 376 | logger.warning("No Cursor path specified or detected") 377 | 378 | logger.info(f"Available projects: {list(db_manager.list_projects().keys())}") 379 | 380 | # Yield empty context - we're using global db_manager instead 381 | yield {} 382 | finally: 383 | # Cleanup on shutdown (if needed) 384 | logger.info("Shutting down Cursor DB MCP server") 385 | 386 | # Create the MCP server with lifespan 387 | mcp = FastMCP("Cursor DB Manager", lifespan=app_lifespan) 388 | 389 | # MCP Resources 390 | @mcp.resource("cursor://projects") 391 | def list_all_projects() -> Dict[str, str]: 392 | """List all available Cursor projects and their database paths""" 393 | global db_manager 394 | return db_manager.list_projects(detailed=False) 395 | 396 | @mcp.resource("cursor://projects/detailed") 397 | def list_detailed_projects() -> Dict[str, Dict[str, Any]]: 398 | """List all available Cursor projects with detailed information""" 399 | global db_manager 400 | return db_manager.list_projects(detailed=True) 401 | 402 | @mcp.resource("cursor://projects/{project_name}/chat") 403 | def get_project_chat_data(project_name: str) -> Dict[str, Any]: 404 | """Retrieve AI chat data from a specific Cursor project""" 405 | global db_manager 406 | try: 407 | return db_manager.get_chat_data(project_name) 408 | except ValueError as e: 409 | return {"error": str(e)} 410 | except Exception as e: 411 | return {"error": f"Error retrieving chat data: {str(e)}"} 412 | 413 | @mcp.resource("cursor://projects/{project_name}/composers") 414 | def get_project_composer_ids(project_name: str) -> Dict[str, Any]: 415 | """Retrieve composer IDs from a specific Cursor project""" 416 | global db_manager 417 | try: 418 | return db_manager.get_composer_ids(project_name) 419 | except ValueError as e: 420 | return {"error": str(e)} 421 | except Exception as e: 422 | return {"error": f"Error retrieving composer data: {str(e)}"} 423 | 424 | @mcp.resource("cursor://composers/{composer_id}") 425 | def get_composer_data_resource(composer_id: str) -> Dict[str, Any]: 426 | """Retrieve composer data from global storage""" 427 | global db_manager 428 | try: 429 | return db_manager.get_composer_data(composer_id) 430 | except ValueError as e: 431 | return {"error": str(e)} 432 | except Exception as e: 433 | return {"error": f"Error retrieving composer data: {str(e)}"} 434 | 435 | # MCP Tools 436 | @mcp.tool() 437 | def query_table(project_name: str, table_name: str, query_type: str, key: Optional[str] = None, limit: int = 100) -> List[Dict[str, Any]]: 438 | """ 439 | Query a specific table in a project's database 440 | 441 | Args: 442 | project_name: Name of the project 443 | table_name: Either 'ItemTable' or 'cursorDiskKV' 444 | query_type: Type of query ('get_all', 'get_by_key', 'search_keys') 445 | key: Key to search for when using 'get_by_key' or 'search_keys' 446 | limit: Maximum number of results to return 447 | 448 | Returns: 449 | List of query results 450 | """ 451 | global db_manager 452 | try: 453 | return db_manager.execute_query(project_name, table_name, query_type, key, limit) 454 | except ValueError as e: 455 | return [{"error": str(e)}] 456 | except sqlite3.Error as e: 457 | return [{"error": f"Database error: {str(e)}"}] 458 | 459 | @mcp.tool() 460 | def refresh_databases() -> Dict[str, Any]: 461 | """Refresh the list of database paths""" 462 | global db_manager 463 | db_manager.refresh_db_paths() 464 | return { 465 | "message": "Database paths refreshed", 466 | "projects": db_manager.list_projects() 467 | } 468 | 469 | # @mcp.tool() 470 | # def add_project_directory(project_dir: str) -> Dict[str, Any]: 471 | # """ 472 | # Add a new project directory to the manager 473 | 474 | # Args: 475 | # project_dir: Path to the project directory 476 | 477 | # Returns: 478 | # Result of the operation 479 | # """ 480 | # global db_manager 481 | # try: 482 | # count = db_manager.add_project_dir(project_dir) 483 | # return { 484 | # "message": f"Project directory added. Total projects: {count}", 485 | # "projects": db_manager.list_projects() 486 | # } 487 | # except Exception as e: 488 | # return {"error": f"Error adding project directory: {str(e)}"} 489 | 490 | # MCP Prompts 491 | @mcp.prompt() 492 | def explore_cursor_projects() -> str: 493 | """Create a prompt to explore Cursor projects""" 494 | return """ 495 | I can help you explore your Cursor projects and their data. 496 | 497 | Here are some things I can do: 498 | 1. List all your Cursor projects 499 | 2. Show AI chat history from a project 500 | 3. Find composer data 501 | 4. Query specific tables in the Cursor database 502 | 503 | What would you like to explore? 504 | """ 505 | 506 | @mcp.prompt() 507 | def analyze_chat_data(project_name: str) -> str: 508 | """Create a prompt to analyze chat data from a specific project""" 509 | return f""" 510 | I'll analyze the AI chat data from your '{project_name}' project. 511 | 512 | I can help you understand: 513 | - The conversation history 514 | - Code snippets shared in the chat 515 | - Common themes or questions 516 | 517 | Would you like me to focus on any specific aspect of the chat data? 518 | """ -------------------------------------------------------------------------------- /img/claude-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/claude-logo.png -------------------------------------------------------------------------------- /img/cursor-db-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-db-keys.png -------------------------------------------------------------------------------- /img/cursor-db-mcp-claude.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-db-mcp-claude.gif -------------------------------------------------------------------------------- /img/cursor-db-mcp-in-cursor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-db-mcp-in-cursor.gif -------------------------------------------------------------------------------- /img/cursor-db-mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-db-mcp.png -------------------------------------------------------------------------------- /img/cursor-db-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-db-structure.png -------------------------------------------------------------------------------- /img/cursor-journal-logo_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/cursor-journal-logo_thumbnail.jpg -------------------------------------------------------------------------------- /img/mcp-cursor-db-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbdamask/cursor-db-mcp/719be85c0f8bba89e03cd53230b0a6450194342c/img/mcp-cursor-db-search.png -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Installation script for the Cursor DB MCP server. 4 | This script creates a virtual environment and installs all necessary dependencies, 5 | including the MCP CLI, into that isolated environment. 6 | """ 7 | 8 | import subprocess 9 | import sys 10 | import os 11 | import platform 12 | import shutil 13 | import site 14 | 15 | def create_and_setup_venv(): 16 | """Create a virtual environment and return the path to its Python executable.""" 17 | venv_dir = ".venv" 18 | 19 | # Check if venv already exists 20 | if os.path.exists(venv_dir): 21 | print(f"Virtual environment already exists at ./{venv_dir}") 22 | should_recreate = input("Do you want to recreate it? (y/n): ").lower().strip() 23 | if should_recreate == 'y': 24 | print(f"Removing existing virtual environment at ./{venv_dir}...") 25 | shutil.rmtree(venv_dir) 26 | else: 27 | print(f"Using existing virtual environment at ./{venv_dir}") 28 | 29 | # Create venv if it doesn't exist or was removed 30 | if not os.path.exists(venv_dir): 31 | print(f"\nCreating virtual environment in ./{venv_dir}...") 32 | try: 33 | # Use the built-in venv module 34 | subprocess.check_call([sys.executable, "-m", "venv", venv_dir]) 35 | except subprocess.CalledProcessError: 36 | print("Failed to create virtual environment using venv module.") 37 | print("Please make sure you have the venv module installed.") 38 | sys.exit(1) 39 | 40 | # Determine the path to the Python executable in the virtual environment 41 | if platform.system() == "Windows": 42 | python_path = os.path.join(venv_dir, "Scripts", "python.exe") 43 | pip_path = os.path.join(venv_dir, "Scripts", "pip.exe") 44 | else: 45 | python_path = os.path.join(venv_dir, "bin", "python") 46 | pip_path = os.path.join(venv_dir, "bin", "pip") 47 | 48 | # Verify the virtual environment was created successfully 49 | if not os.path.exists(python_path): 50 | print(f"Error: Could not find Python executable at {python_path}") 51 | print("Virtual environment creation may have failed.") 52 | sys.exit(1) 53 | 54 | return python_path 55 | 56 | def main(): 57 | print("Setting up Cursor DB MCP server...") 58 | 59 | # Create virtual environment and get the Python path 60 | python_path = create_and_setup_venv() 61 | 62 | # Upgrade pip in the virtual environment 63 | print("\nUpgrading pip in the virtual environment...") 64 | subprocess.check_call([python_path, "-m", "pip", "install", "--upgrade", "pip"]) 65 | 66 | # Install basic dependencies 67 | print("\nInstalling basic dependencies...") 68 | subprocess.check_call([python_path, "-m", "pip", "install", "-r", "requirements.txt"]) 69 | 70 | # # Install MCP CLI dependencies 71 | # print("\nInstalling MCP CLI dependencies...") 72 | # try: 73 | # # Try to install with quotes to handle square brackets 74 | # subprocess.check_call([python_path, "-m", "pip", "install", "mcp[cli]"]) 75 | # except subprocess.CalledProcessError: 76 | # # If that fails, install the dependencies directly 77 | # print("Direct installation of mcp[cli] failed. Installing CLI dependencies individually...") 78 | # subprocess.check_call([python_path, "-m", "pip", "install", "mcp", "typer>=0.9.0", "rich>=13.0.0"]) 79 | 80 | print("\nInstallation completed successfully!") 81 | 82 | # # Print activation instructions 83 | # venv_dir = "venv" 84 | # print(f"\nTo use the Cursor DB MCP server, you need to activate the virtual environment:") 85 | # if platform.system() == "Windows": 86 | # print(f" {venv_dir}\\Scripts\\activate") 87 | # else: 88 | # print(f" source {venv_dir}/bin/activate") 89 | 90 | # print("\nAfter activation, you can test the MCP server with:") 91 | # print(" python test_mcp_server.py") 92 | 93 | 94 | if __name__ == "__main__": 95 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.0.0 2 | pathlib>=1.0.1 3 | typing>=3.7.4.3 -------------------------------------------------------------------------------- /test_mcp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for the Cursor DB MCP server. 4 | This script starts the MCP server and performs tests using the MCP Python SDK. 5 | """ 6 | 7 | import subprocess 8 | import time 9 | import sys 10 | import os 11 | import json 12 | import asyncio 13 | from pathlib import Path 14 | 15 | # Import MCP client libraries with correct paths 16 | from mcp import ClientSession, StdioServerParameters 17 | from mcp.client.stdio import stdio_client 18 | 19 | 20 | async def test_mcp_server(): 21 | """Test the MCP server using the Python SDK""" 22 | print("Starting Cursor DB MCP server test...") 23 | 24 | try: 25 | print("\nTesting MCP server with the Python SDK...") 26 | 27 | # Use stdio_client to start the server and connect to it 28 | server_params = StdioServerParameters( 29 | command=sys.executable, 30 | args=["cursor-db-mcp-server.py"], 31 | env=None 32 | ) 33 | 34 | async with stdio_client(server_params) as (read, write): 35 | print("✅ Successfully started MCP server process") 36 | 37 | async with ClientSession(read, write) as session: 38 | # Initialize the connection 39 | await session.initialize() 40 | print("✅ Successfully initialized connection to MCP server") 41 | 42 | # Test 1: List available resources 43 | print("\n1. Testing resource listing...") 44 | resources = await session.list_resources() 45 | 46 | if resources and any(hasattr(r, 'uri') and "cursor://projects" in r.uri for r in resources): 47 | print("✅ Successfully listed resources") 48 | print(f"Resources: {[getattr(r, 'uri', str(r)) for r in resources]}") 49 | else: 50 | print("❌ Failed to find expected resources") 51 | print(f"Resources found: {resources}") 52 | 53 | # Test 2: List available tools 54 | print("\n2. Testing tool listing...") 55 | tools = await session.list_tools() 56 | 57 | if tools and any(hasattr(t, 'name') and "query_table" in t.name for t in tools): 58 | print("✅ Successfully listed tools") 59 | print(f"Tools: {[getattr(t, 'name', str(t)) for t in tools]}") 60 | else: 61 | print("❌ Failed to find expected tools") 62 | print(f"Tools found: {tools}") 63 | 64 | # Test 3: List available prompts 65 | print("\n3. Testing prompt listing...") 66 | prompts = await session.list_prompts() 67 | 68 | if prompts and any(hasattr(p, 'name') and "explore_cursor_projects" in p.name for p in prompts): 69 | print("✅ Successfully listed prompts") 70 | print(f"Prompts: {[getattr(p, 'name', str(p)) for p in prompts]}") 71 | else: 72 | print("❌ Failed to find expected prompts") 73 | print(f"Prompts found: {prompts}") 74 | 75 | # Test 4: Call a tool 76 | print("\n4. Testing tool execution...") 77 | try: 78 | # Get list of projects first 79 | projects_result = await session.call_tool("cursorprojects") 80 | 81 | if projects_result and isinstance(projects_result, list): 82 | print("✅ Successfully called cursorprojects tool") 83 | print(f"Projects: {projects_result}") 84 | 85 | # If we have projects, try querying one 86 | if projects_result: 87 | project_name = projects_result[0] # Use the first project 88 | query_result = await session.call_tool( 89 | "query_table", 90 | arguments={ 91 | "project_name": project_name, 92 | "table_name": "ItemTable", 93 | "query_type": "get_all", 94 | "limit": 5 95 | } 96 | ) 97 | 98 | if query_result: 99 | print("✅ Successfully queried project database") 100 | print(f"Query result: {query_result[:2]}...") # Show first 2 items 101 | else: 102 | print("❌ Failed to query project database") 103 | else: 104 | print("❌ Failed to call cursorprojects tool") 105 | print(f"Result: {projects_result}") 106 | except Exception as e: 107 | print(f"❌ Error calling tool: {e}") 108 | 109 | # Test 5: Read a resource 110 | print("\n5. Testing resource reading...") 111 | try: 112 | # Try to read the projects resource 113 | content, mime_type = await session.read_resource("cursor://projects") 114 | 115 | if content: 116 | print("✅ Successfully read cursor://projects resource") 117 | print(f"Content type: {mime_type}") 118 | print(f"Content preview: {content[:100]}...") # Show first 100 chars 119 | else: 120 | print("❌ Failed to read cursor://projects resource") 121 | except Exception as e: 122 | print(f"❌ Error reading resource: {e}") 123 | 124 | print("\nAll tests completed!") 125 | 126 | except Exception as e: 127 | print(f"Error during testing: {e}") 128 | 129 | print("Test completed.") 130 | 131 | 132 | def main(): 133 | """Main entry point""" 134 | asyncio.run(test_mcp_server()) 135 | 136 | 137 | if __name__ == "__main__": 138 | main() --------------------------------------------------------------------------------