pylance-mcp-server 1.0.0

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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/bin/pylance-mcp.js +68 -0
  4. package/mcp_server/__init__.py +13 -0
  5. package/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/mcp_server/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/mcp_server/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/mcp_server/__pycache__/ai_features.cpython-313.pyc +0 -0
  9. package/mcp_server/__pycache__/api_routes.cpython-313.pyc +0 -0
  10. package/mcp_server/__pycache__/auth.cpython-313.pyc +0 -0
  11. package/mcp_server/__pycache__/cloud_sync.cpython-313.pyc +0 -0
  12. package/mcp_server/__pycache__/logging_db.cpython-312.pyc +0 -0
  13. package/mcp_server/__pycache__/logging_db.cpython-313.pyc +0 -0
  14. package/mcp_server/__pycache__/pylance_bridge.cpython-312.pyc +0 -0
  15. package/mcp_server/__pycache__/pylance_bridge.cpython-313.pyc +0 -0
  16. package/mcp_server/__pycache__/pylance_bridge.cpython-314.pyc +0 -0
  17. package/mcp_server/__pycache__/resources.cpython-312.pyc +0 -0
  18. package/mcp_server/__pycache__/resources.cpython-313.pyc +0 -0
  19. package/mcp_server/__pycache__/tools.cpython-312.pyc +0 -0
  20. package/mcp_server/__pycache__/tools.cpython-313.pyc +0 -0
  21. package/mcp_server/__pycache__/tracing.cpython-313.pyc +0 -0
  22. package/mcp_server/ai_features.py +274 -0
  23. package/mcp_server/api_routes.py +429 -0
  24. package/mcp_server/auth.py +275 -0
  25. package/mcp_server/cloud_sync.py +427 -0
  26. package/mcp_server/logging_db.py +403 -0
  27. package/mcp_server/pylance_bridge.py +579 -0
  28. package/mcp_server/resources.py +174 -0
  29. package/mcp_server/tools.py +642 -0
  30. package/mcp_server/tracing.py +84 -0
  31. package/package.json +53 -0
  32. package/requirements.txt +29 -0
  33. package/scripts/check-python.js +57 -0
  34. package/server.py +1228 -0
@@ -0,0 +1,403 @@
1
+ """
2
+ Conversation Logging Database
3
+ Logs all user prompts, LLM responses, and tool calls for training data collection.
4
+ """
5
+
6
+ import sqlite3
7
+ import json
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional, Dict, Any, List
11
+ from datetime import datetime
12
+ from contextlib import contextmanager
13
+
14
+
15
+ class ConversationLogger:
16
+ """SQLite database for logging MCP conversations and tool usage."""
17
+
18
+ def __init__(self, db_path: Optional[str] = None):
19
+ """
20
+ Initialize conversation logger.
21
+
22
+ Args:
23
+ db_path: Path to SQLite database file. Defaults to ~/.pylance_mcp/conversations.db
24
+ """
25
+ if db_path is None:
26
+ db_dir = Path.home() / ".pylance_mcp"
27
+ db_dir.mkdir(parents=True, exist_ok=True)
28
+ db_path = str(db_dir / "conversations.db")
29
+
30
+ self.db_path = db_path
31
+ self._init_database()
32
+
33
+ def _init_database(self):
34
+ """Create database tables if they don't exist."""
35
+ with self._get_connection() as conn:
36
+ cursor = conn.cursor()
37
+
38
+ # Conversations table - tracks user sessions
39
+ cursor.execute("""
40
+ CREATE TABLE IF NOT EXISTS conversations (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ session_id TEXT UNIQUE NOT NULL,
43
+ workspace_root TEXT,
44
+ python_version TEXT,
45
+ venv_path TEXT,
46
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
+ ended_at TIMESTAMP,
48
+ total_messages INTEGER DEFAULT 0,
49
+ total_tool_calls INTEGER DEFAULT 0,
50
+ metadata TEXT
51
+ )
52
+ """)
53
+
54
+ # Messages table - logs every user prompt and LLM response
55
+ cursor.execute("""
56
+ CREATE TABLE IF NOT EXISTS messages (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ conversation_id INTEGER NOT NULL,
59
+ message_type TEXT NOT NULL,
60
+ role TEXT NOT NULL,
61
+ content TEXT NOT NULL,
62
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
63
+ token_count INTEGER,
64
+ model_name TEXT,
65
+ metadata TEXT,
66
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
67
+ )
68
+ """)
69
+
70
+ # Tool calls table - logs every MCP tool invocation
71
+ cursor.execute("""
72
+ CREATE TABLE IF NOT EXISTS tool_calls (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ message_id INTEGER,
75
+ conversation_id INTEGER NOT NULL,
76
+ tool_name TEXT NOT NULL,
77
+ arguments TEXT NOT NULL,
78
+ result TEXT,
79
+ success BOOLEAN,
80
+ error_message TEXT,
81
+ execution_time_ms REAL,
82
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
83
+ FOREIGN KEY (message_id) REFERENCES messages(id),
84
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
85
+ )
86
+ """)
87
+
88
+ # Files modified table - tracks which files were changed
89
+ cursor.execute("""
90
+ CREATE TABLE IF NOT EXISTS files_modified (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ tool_call_id INTEGER NOT NULL,
93
+ file_path TEXT NOT NULL,
94
+ operation TEXT NOT NULL,
95
+ lines_added INTEGER DEFAULT 0,
96
+ lines_removed INTEGER DEFAULT 0,
97
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
98
+ FOREIGN KEY (tool_call_id) REFERENCES tool_calls(id)
99
+ )
100
+ """)
101
+
102
+ # Training data view - optimized for LLM training
103
+ cursor.execute("""
104
+ CREATE VIEW IF NOT EXISTS training_data AS
105
+ SELECT
106
+ c.session_id,
107
+ c.workspace_root,
108
+ m.id as message_id,
109
+ m.role,
110
+ m.content,
111
+ m.timestamp,
112
+ m.model_name,
113
+ GROUP_CONCAT(tc.tool_name || '(' || tc.arguments || ')') as tools_used,
114
+ GROUP_CONCAT(fm.file_path) as files_modified,
115
+ COUNT(DISTINCT tc.id) as tool_call_count
116
+ FROM conversations c
117
+ JOIN messages m ON c.id = m.conversation_id
118
+ LEFT JOIN tool_calls tc ON m.id = tc.message_id
119
+ LEFT JOIN files_modified fm ON tc.id = fm.tool_call_id
120
+ GROUP BY m.id
121
+ ORDER BY m.timestamp
122
+ """)
123
+
124
+ # Create indexes for performance
125
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)")
126
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)")
127
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_tool_calls_conversation ON tool_calls(conversation_id)")
128
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_tool_calls_tool_name ON tool_calls(tool_name)")
129
+
130
+ conn.commit()
131
+
132
+ @contextmanager
133
+ def _get_connection(self):
134
+ """Context manager for database connections."""
135
+ conn = sqlite3.connect(self.db_path)
136
+ conn.row_factory = sqlite3.Row
137
+ try:
138
+ yield conn
139
+ finally:
140
+ conn.close()
141
+
142
+ def start_conversation(self, session_id: str, workspace_root: str,
143
+ python_version: Optional[str] = None,
144
+ venv_path: Optional[str] = None,
145
+ metadata: Optional[Dict[str, Any]] = None) -> int:
146
+ """
147
+ Start a new conversation session.
148
+
149
+ Args:
150
+ session_id: Unique session identifier
151
+ workspace_root: Path to the workspace
152
+ python_version: Python version being used
153
+ venv_path: Virtual environment path
154
+ metadata: Additional metadata as dict
155
+
156
+ Returns:
157
+ conversation_id: Database ID for this conversation
158
+ """
159
+ with self._get_connection() as conn:
160
+ cursor = conn.cursor()
161
+ cursor.execute("""
162
+ INSERT INTO conversations (session_id, workspace_root, python_version, venv_path, metadata)
163
+ VALUES (?, ?, ?, ?, ?)
164
+ """, (session_id, workspace_root, python_version, venv_path,
165
+ json.dumps(metadata) if metadata else None))
166
+ conn.commit()
167
+ return cursor.lastrowid
168
+
169
+ def end_conversation(self, conversation_id: int):
170
+ """Mark conversation as ended."""
171
+ with self._get_connection() as conn:
172
+ cursor = conn.cursor()
173
+ cursor.execute("""
174
+ UPDATE conversations
175
+ SET ended_at = CURRENT_TIMESTAMP,
176
+ total_messages = (SELECT COUNT(*) FROM messages WHERE conversation_id = ?),
177
+ total_tool_calls = (SELECT COUNT(*) FROM tool_calls WHERE conversation_id = ?)
178
+ WHERE id = ?
179
+ """, (conversation_id, conversation_id, conversation_id))
180
+ conn.commit()
181
+
182
+ def log_user_message(self, conversation_id: int, content: str,
183
+ metadata: Optional[Dict[str, Any]] = None) -> int:
184
+ """
185
+ Log a user message (prompt).
186
+
187
+ Args:
188
+ conversation_id: ID of the conversation
189
+ content: User's message content
190
+ metadata: Additional metadata
191
+
192
+ Returns:
193
+ message_id: Database ID for this message
194
+ """
195
+ return self._log_message(conversation_id, "user_prompt", "user", content, metadata=metadata)
196
+
197
+ def log_assistant_message(self, conversation_id: int, content: str,
198
+ model_name: Optional[str] = None,
199
+ token_count: Optional[int] = None,
200
+ metadata: Optional[Dict[str, Any]] = None) -> int:
201
+ """
202
+ Log an assistant message (LLM response).
203
+
204
+ Args:
205
+ conversation_id: ID of the conversation
206
+ content: Assistant's response content
207
+ model_name: Name of the LLM model used
208
+ token_count: Number of tokens in response
209
+ metadata: Additional metadata
210
+
211
+ Returns:
212
+ message_id: Database ID for this message
213
+ """
214
+ return self._log_message(
215
+ conversation_id, "assistant_response", "assistant", content,
216
+ model_name=model_name, token_count=token_count, metadata=metadata
217
+ )
218
+
219
+ def _log_message(self, conversation_id: int, message_type: str, role: str,
220
+ content: str, model_name: Optional[str] = None,
221
+ token_count: Optional[int] = None,
222
+ metadata: Optional[Dict[str, Any]] = None) -> int:
223
+ """Internal method to log a message."""
224
+ with self._get_connection() as conn:
225
+ cursor = conn.cursor()
226
+ cursor.execute("""
227
+ INSERT INTO messages (conversation_id, message_type, role, content,
228
+ model_name, token_count, metadata)
229
+ VALUES (?, ?, ?, ?, ?, ?, ?)
230
+ """, (conversation_id, message_type, role, content, model_name, token_count,
231
+ json.dumps(metadata) if metadata else None))
232
+ conn.commit()
233
+ return cursor.lastrowid
234
+
235
+ def log_tool_call(self, conversation_id: int, tool_name: str, arguments: Dict[str, Any],
236
+ result: Optional[Any] = None, success: bool = True,
237
+ error_message: Optional[str] = None, execution_time_ms: Optional[float] = None,
238
+ message_id: Optional[int] = None) -> int:
239
+ """
240
+ Log a tool call.
241
+
242
+ Args:
243
+ conversation_id: ID of the conversation
244
+ tool_name: Name of the tool called
245
+ arguments: Tool arguments as dict
246
+ result: Tool execution result
247
+ success: Whether the tool call succeeded
248
+ error_message: Error message if failed
249
+ execution_time_ms: Execution time in milliseconds
250
+ message_id: Associated message ID (optional)
251
+
252
+ Returns:
253
+ tool_call_id: Database ID for this tool call
254
+ """
255
+ with self._get_connection() as conn:
256
+ cursor = conn.cursor()
257
+ cursor.execute("""
258
+ INSERT INTO tool_calls (conversation_id, message_id, tool_name, arguments,
259
+ result, success, error_message, execution_time_ms)
260
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
261
+ """, (conversation_id, message_id, tool_name, json.dumps(arguments),
262
+ json.dumps(result) if result else None, success, error_message, execution_time_ms))
263
+ conn.commit()
264
+ return cursor.lastrowid
265
+
266
+ def log_file_modification(self, tool_call_id: int, file_path: str, operation: str,
267
+ lines_added: int = 0, lines_removed: int = 0):
268
+ """
269
+ Log a file modification.
270
+
271
+ Args:
272
+ tool_call_id: ID of the tool call that modified the file
273
+ file_path: Path to the modified file
274
+ operation: Type of operation (edit, create, delete, rename)
275
+ lines_added: Number of lines added
276
+ lines_removed: Number of lines removed
277
+ """
278
+ with self._get_connection() as conn:
279
+ cursor = conn.cursor()
280
+ cursor.execute("""
281
+ INSERT INTO files_modified (tool_call_id, file_path, operation,
282
+ lines_added, lines_removed)
283
+ VALUES (?, ?, ?, ?, ?)
284
+ """, (tool_call_id, file_path, operation, lines_added, lines_removed))
285
+ conn.commit()
286
+
287
+ def get_conversation(self, conversation_id: int) -> Optional[Dict[str, Any]]:
288
+ """Get conversation details."""
289
+ with self._get_connection() as conn:
290
+ cursor = conn.cursor()
291
+ cursor.execute("SELECT * FROM conversations WHERE id = ?", (conversation_id,))
292
+ row = cursor.fetchone()
293
+ return dict(row) if row else None
294
+
295
+ def get_messages(self, conversation_id: int) -> List[Dict[str, Any]]:
296
+ """Get all messages in a conversation."""
297
+ with self._get_connection() as conn:
298
+ cursor = conn.cursor()
299
+ cursor.execute("""
300
+ SELECT * FROM messages
301
+ WHERE conversation_id = ?
302
+ ORDER BY timestamp
303
+ """, (conversation_id,))
304
+ return [dict(row) for row in cursor.fetchall()]
305
+
306
+ def get_tool_calls(self, conversation_id: int) -> List[Dict[str, Any]]:
307
+ """Get all tool calls in a conversation."""
308
+ with self._get_connection() as conn:
309
+ cursor = conn.cursor()
310
+ cursor.execute("""
311
+ SELECT * FROM tool_calls
312
+ WHERE conversation_id = ?
313
+ ORDER BY timestamp
314
+ """, (conversation_id,))
315
+ return [dict(row) for row in cursor.fetchall()]
316
+
317
+ def export_training_data(self, output_path: str, format: str = "jsonl"):
318
+ """
319
+ Export conversation data for LLM training.
320
+
321
+ Args:
322
+ output_path: Path to output file
323
+ format: Export format ("jsonl" or "json")
324
+ """
325
+ with self._get_connection() as conn:
326
+ cursor = conn.cursor()
327
+ cursor.execute("SELECT * FROM training_data")
328
+ rows = cursor.fetchall()
329
+
330
+ training_examples = []
331
+ for row in rows:
332
+ example = dict(row)
333
+ training_examples.append(example)
334
+
335
+ output_file = Path(output_path)
336
+ if format == "jsonl":
337
+ with open(output_file, 'w', encoding='utf-8') as f:
338
+ for example in training_examples:
339
+ f.write(json.dumps(example) + '\n')
340
+ else: # json
341
+ with open(output_file, 'w', encoding='utf-8') as f:
342
+ json.dump(training_examples, f, indent=2)
343
+
344
+ def get_statistics(self) -> Dict[str, Any]:
345
+ """Get overall statistics from the database."""
346
+ with self._get_connection() as conn:
347
+ cursor = conn.cursor()
348
+
349
+ stats = {}
350
+
351
+ # Total conversations
352
+ cursor.execute("SELECT COUNT(*) as count FROM conversations")
353
+ stats['total_conversations'] = cursor.fetchone()['count']
354
+
355
+ # Total messages
356
+ cursor.execute("SELECT COUNT(*) as count FROM messages")
357
+ stats['total_messages'] = cursor.fetchone()['count']
358
+
359
+ # Total tool calls
360
+ cursor.execute("SELECT COUNT(*) as count FROM tool_calls")
361
+ stats['total_tool_calls'] = cursor.fetchone()['count']
362
+
363
+ # Most used tools
364
+ cursor.execute("""
365
+ SELECT tool_name, COUNT(*) as count
366
+ FROM tool_calls
367
+ GROUP BY tool_name
368
+ ORDER BY count DESC
369
+ LIMIT 10
370
+ """)
371
+ stats['top_tools'] = [dict(row) for row in cursor.fetchall()]
372
+
373
+ # Average tools per conversation
374
+ cursor.execute("""
375
+ SELECT AVG(total_tool_calls) as avg_tools
376
+ FROM conversations
377
+ WHERE total_tool_calls > 0
378
+ """)
379
+ stats['avg_tools_per_conversation'] = cursor.fetchone()['avg_tools']
380
+
381
+ # Files modified count
382
+ cursor.execute("SELECT COUNT(DISTINCT file_path) as count FROM files_modified")
383
+ stats['unique_files_modified'] = cursor.fetchone()['count']
384
+
385
+ return stats
386
+
387
+
388
+ # Global logger instance
389
+ _logger: Optional[ConversationLogger] = None
390
+
391
+
392
+ def get_logger() -> ConversationLogger:
393
+ """Get or create the global conversation logger."""
394
+ global _logger
395
+ if _logger is None:
396
+ _logger = ConversationLogger()
397
+ return _logger
398
+
399
+
400
+ def set_logger(logger: ConversationLogger):
401
+ """Set a custom logger instance."""
402
+ global _logger
403
+ _logger = logger