iflow-mcp_jbdamask_cursor-db-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cursor_db_mcp/__init__.py +3 -0
- cursor_db_mcp/__main__.py +4 -0
- cursor_db_mcp/server.py +526 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/METADATA +267 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/RECORD +9 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/licenses/LICENSE +9 -0
- iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/top_level.txt +1 -0
cursor_db_mcp/server.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
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
|
+
"""
|
|
519
|
+
|
|
520
|
+
def main():
|
|
521
|
+
"""Main entry point for the MCP server"""
|
|
522
|
+
mcp.run()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
main()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iflow-mcp_jbdamask_cursor-db-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Model Context Protocol (MCP) server for accessing Cursor IDE's SQLite databases
|
|
5
|
+
Author-email: jbdamask <jbdamask@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jbdamask/cursor-db-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/jbdamask/cursor-db-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/jbdamask/cursor-db-mcp/issues
|
|
10
|
+
Keywords: mcp,cursor,sqlite,database,ide
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: pathlib>=1.0.1
|
|
25
|
+
Requires-Dist: typing>=3.7.4.3
|
|
26
|
+
Provides-Extra: cli
|
|
27
|
+
Requires-Dist: mcp[cli]; extra == "cli"
|
|
28
|
+
Requires-Dist: typer>=0.9.0; extra == "cli"
|
|
29
|
+
Requires-Dist: rich>=13.0.0; extra == "cli"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Cursor DB MCP Server
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
<!-- __Claude__
|
|
37
|
+
 -->
|
|
38
|
+
|
|
39
|
+
__Cursor__
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
Cursor IDE
|
|
46
|
+
<!-- Claude Desktop (if you want to use MCP in Claude) -->
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### Easy Installation
|
|
51
|
+
|
|
52
|
+
Use the provided installation script to install all dependencies:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python install.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This script will install:
|
|
59
|
+
- Basic MCP server and dependencies
|
|
60
|
+
|
|
61
|
+
<!-- ### Manual Installation
|
|
62
|
+
|
|
63
|
+
1. Clone this repository:
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/yourusername/cursor-db-mcp.git
|
|
66
|
+
cd cursor-db-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. Install basic dependencies:
|
|
70
|
+
```bash
|
|
71
|
+
pip install -r requirements.txt
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. Install MCP CLI tools (optional, for testing):
|
|
75
|
+
```bash
|
|
76
|
+
pip install 'mcp[cli]' # Note the quotes around mcp[cli]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the above command fails, you can install the CLI dependencies directly:
|
|
80
|
+
```bash
|
|
81
|
+
pip install typer rich
|
|
82
|
+
``` -->
|
|
83
|
+
|
|
84
|
+
<!-- ## Usage
|
|
85
|
+
|
|
86
|
+
### Using with Claude Desktop
|
|
87
|
+
|
|
88
|
+
1. Install the MCP server in Claude Desktop:
|
|
89
|
+
```bash
|
|
90
|
+
mcp install cursor-db-mcp-server.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
2. In Claude Desktop, you can now access your Cursor data by asking questions like:
|
|
94
|
+
- "Show me a list of my Cursor projects"
|
|
95
|
+
- "What's in my chat history for project X?"
|
|
96
|
+
- "Find composer data for composer ID Y"
|
|
97
|
+
|
|
98
|
+
See detailed examples below
|
|
99
|
+
|
|
100
|
+
Note: If Claude shows an error connecting to this MCP it's likely because it can't find uv. To fix this, change the command value to include the fully qualified path to uv. For example:
|
|
101
|
+
```
|
|
102
|
+
"Cursor DB Manager": {
|
|
103
|
+
"command": "/Users/johndamask/.local/bin/uv",
|
|
104
|
+
"args": [
|
|
105
|
+
"run",
|
|
106
|
+
"--with",
|
|
107
|
+
"mcp[cli]",
|
|
108
|
+
"mcp",
|
|
109
|
+
"run",
|
|
110
|
+
"/Users/johndamask/code/cursor-db-mcp/cursor-db-mcp-server.py"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
``` -->
|
|
114
|
+
|
|
115
|
+
## Using with Cursor IDE
|
|
116
|
+
|
|
117
|
+
1. Open Cursor and navigate to Settings->Cursor Settings->MCP.
|
|
118
|
+
2. Click: Add new MCP server
|
|
119
|
+
3. Name: Cursor DB MCP; Type: Command
|
|
120
|
+
4. Command: \<fully qualified path to\>uv run --with mcp[cli] mcp run \<fully qualified path to\>/cursor-db-mcp-server.py
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
Now you can ask questions about the database or retrieve info about historical chats.
|
|
125
|
+
|
|
126
|
+

|
|
127
|
+
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+
### Using with Claude Desktop
|
|
131
|
+
|
|
132
|
+
[Installing MCP servers for Claude Desktop](https://modelcontextprotocol.io/quickstart/user)
|
|
133
|
+
|
|
134
|
+
Add this to your claude_desktop_config.json file
|
|
135
|
+
```
|
|
136
|
+
"cursor-db-mcp": {
|
|
137
|
+
"command": "<fully qualified path to >/uv",
|
|
138
|
+
"args": [
|
|
139
|
+
"run",
|
|
140
|
+
"--with",
|
|
141
|
+
"mcp[cli]",
|
|
142
|
+
"mcp",
|
|
143
|
+
"run",
|
|
144
|
+
"<fully qualified path to >/cursor-db-mcp-server.py"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+

|
|
151
|
+
|
|
152
|
+
## Available Resources
|
|
153
|
+
|
|
154
|
+
- `cursor://projects` - List all available Cursor projects
|
|
155
|
+
- `cursor://projects/detailed` - List projects with detailed information
|
|
156
|
+
- `cursor://projects/{project_name}/chat` - Get chat data for a specific project
|
|
157
|
+
- `cursor://projects/{project_name}/composers` - Get composer IDs for a specific project
|
|
158
|
+
- `cursor://composers/{composer_id}` - Get data for a specific composer
|
|
159
|
+
|
|
160
|
+
## Available Tools
|
|
161
|
+
|
|
162
|
+
- `query_table` - Query a specific table in a project's database
|
|
163
|
+
- `refresh_databases` - Refresh the list of database paths
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
<!-- # Example Usage with Claude
|
|
167
|
+
|
|
168
|
+
## Listing Projects
|
|
169
|
+
|
|
170
|
+
**User**: "Can you show me a list of my Cursor projects?"
|
|
171
|
+
|
|
172
|
+
**Claude**:
|
|
173
|
+
```
|
|
174
|
+
I'll retrieve a list of your Cursor projects.
|
|
175
|
+
|
|
176
|
+
Here are the Cursor projects I found:
|
|
177
|
+
- project1 (path: /Users/username/Library/Application Support/Cursor/User/workspaceStorage/abc123/state.vscdb)
|
|
178
|
+
- project2 (path: /Users/username/Library/Application Support/Cursor/User/workspaceStorage/def456/state.vscdb)
|
|
179
|
+
- custom-project (path: /Users/username/code/custom-project/state.vscdb)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Viewing Chat History
|
|
183
|
+
|
|
184
|
+
**User**: "Show me the chat history from my 'project1' project."
|
|
185
|
+
|
|
186
|
+
**Claude**:
|
|
187
|
+
```
|
|
188
|
+
I'll retrieve the chat history from your 'project1' project.
|
|
189
|
+
|
|
190
|
+
Here's the chat history I found:
|
|
191
|
+
|
|
192
|
+
Chat: "Understanding React Hooks"
|
|
193
|
+
- You: "Can you explain how useEffect works in React?"
|
|
194
|
+
- Claude: "The useEffect hook in React is used for handling side effects in functional components..."
|
|
195
|
+
|
|
196
|
+
Chat: "Debugging API Connection"
|
|
197
|
+
- You: "I'm getting a 401 error when connecting to the API. Here's my code..."
|
|
198
|
+
- Claude: "It looks like you're missing the authorization header in your request..."
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Querying Database Tables
|
|
202
|
+
|
|
203
|
+
**User**: "Can you search for keys related to 'settings' in the ItemTable of my 'project1' project?"
|
|
204
|
+
|
|
205
|
+
**Claude**:
|
|
206
|
+
```
|
|
207
|
+
I'll search for keys related to 'settings' in the ItemTable of your 'project1' project.
|
|
208
|
+
|
|
209
|
+
Here are the results:
|
|
210
|
+
1. Key: "workbench.settings.editor"
|
|
211
|
+
Value: {"mode": "ui", "lastFocusedSettingId": "editor.fontSize"}
|
|
212
|
+
|
|
213
|
+
2. Key: "settings.language"
|
|
214
|
+
Value: "en"
|
|
215
|
+
|
|
216
|
+
3. Key: "settings.theme"
|
|
217
|
+
Value: "dark"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Analyzing Chat Data
|
|
221
|
+
|
|
222
|
+
**User**: "Can you analyze the chat data from my 'project1' project and tell me what topics I've been discussing most frequently?"
|
|
223
|
+
|
|
224
|
+
**Claude**:
|
|
225
|
+
```
|
|
226
|
+
I'll analyze the chat data from your 'project1' project.
|
|
227
|
+
|
|
228
|
+
Based on your chat history, here are the most frequent topics you've been discussing:
|
|
229
|
+
1. React Hooks and Components (5 conversations)
|
|
230
|
+
2. API Integration (3 conversations)
|
|
231
|
+
3. CSS Styling (2 conversations)
|
|
232
|
+
4. Performance Optimization (2 conversations)
|
|
233
|
+
5. Debugging (1 conversation)
|
|
234
|
+
|
|
235
|
+
The most common questions were about state management in React and handling API responses.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
# Architecture
|
|
239
|
+
|
|
240
|
+
The server uses the Model Context Protocol (MCP) to expose Cursor's SQLite databases to AI assistants. Key components include:
|
|
241
|
+
|
|
242
|
+
1. **Lifespan Context Management**: The server uses MCP's lifespan API to efficiently manage resources throughout the server's lifecycle.
|
|
243
|
+
|
|
244
|
+
2. **CursorDBManager**: Handles the detection and management of Cursor projects and their databases.
|
|
245
|
+
|
|
246
|
+
3. **Resources**: Expose data from Cursor databases as MCP resources.
|
|
247
|
+
|
|
248
|
+
4. **Tools**: Provide functionality to query databases and manage projects.
|
|
249
|
+
|
|
250
|
+
5. **Prompts**: Define reusable templates for AI interactions. -->
|
|
251
|
+
|
|
252
|
+
# How It Works
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
# Notes
|
|
257
|
+
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.
|
|
258
|
+
2. This was written on a Mac. YMMV with other OS
|
|
259
|
+
|
|
260
|
+
# Shameless Plug
|
|
261
|
+
<img src="./img/cursor-journal-logo_thumbnail.jpg" width="150" />
|
|
262
|
+
|
|
263
|
+
Like this? Try [Cursor Journal](https://medium.com/@jbdamask/building-cursor-journal-with-cursor-77445026a08c) to create DevLogs directly from Cursor chat history!
|
|
264
|
+
|
|
265
|
+
# License
|
|
266
|
+
|
|
267
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cursor_db_mcp/__init__.py,sha256=dYEmeymUVHZvAvSxzHqIqLKzJ3On7Xua1IMCkv8j1GM,57
|
|
2
|
+
cursor_db_mcp/__main__.py,sha256=AKgOIhm54q3wlx2YqNK-4ilZKzyYkPmvOguLYk6CSE8,69
|
|
3
|
+
cursor_db_mcp/server.py,sha256=mOeOIhpUoXgZcOT-r9ZnSR2q6l8EWYnIN-xwXU79g6A,19492
|
|
4
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=Tr947TjTB1T7aX_yOgFF6KWyZpP2P1YOOagjvk2MgeQ,1085
|
|
5
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/METADATA,sha256=Zr64CuGbxEOe5oJPtNsaBUjQNNi_Es2HFZUHdiY2U2o,8323
|
|
6
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/entry_points.txt,sha256=HYeWvDKRZ_rXBuZwecLQ0WDMWZQORLNmK-nIgVbsrdA,60
|
|
8
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/top_level.txt,sha256=e-V_1dKY7oD4hFQZQVS3c1x49Df_U7-oBNKc9QmKz0U,14
|
|
9
|
+
iflow_mcp_jbdamask_cursor_db_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cursor_db_mcp
|