devlake-mcp 0.4.1__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.
- devlake_mcp/__init__.py +7 -0
- devlake_mcp/__main__.py +10 -0
- devlake_mcp/cli.py +794 -0
- devlake_mcp/client.py +474 -0
- devlake_mcp/compat.py +165 -0
- devlake_mcp/config.py +204 -0
- devlake_mcp/constants.py +161 -0
- devlake_mcp/enums.py +58 -0
- devlake_mcp/generation_manager.py +296 -0
- devlake_mcp/git_utils.py +489 -0
- devlake_mcp/hooks/__init__.py +49 -0
- devlake_mcp/hooks/hook_utils.py +246 -0
- devlake_mcp/hooks/post_tool_use.py +325 -0
- devlake_mcp/hooks/pre_tool_use.py +110 -0
- devlake_mcp/hooks/record_session.py +183 -0
- devlake_mcp/hooks/session_start.py +81 -0
- devlake_mcp/hooks/stop.py +275 -0
- devlake_mcp/hooks/transcript_utils.py +547 -0
- devlake_mcp/hooks/user_prompt_submit.py +204 -0
- devlake_mcp/logging_config.py +202 -0
- devlake_mcp/retry_queue.py +556 -0
- devlake_mcp/server.py +664 -0
- devlake_mcp/session_manager.py +444 -0
- devlake_mcp/utils.py +225 -0
- devlake_mcp/version_utils.py +174 -0
- devlake_mcp-0.4.1.dist-info/METADATA +541 -0
- devlake_mcp-0.4.1.dist-info/RECORD +31 -0
- devlake_mcp-0.4.1.dist-info/WHEEL +5 -0
- devlake_mcp-0.4.1.dist-info/entry_points.txt +3 -0
- devlake_mcp-0.4.1.dist-info/licenses/LICENSE +21 -0
- devlake_mcp-0.4.1.dist-info/top_level.txt +1 -0
devlake_mcp/server.py
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevLake MCP 服务器实现
|
|
3
|
+
|
|
4
|
+
使用 FastMCP 框架实现 MCP 服务器,提供与 DevLake 交互的工具。
|
|
5
|
+
|
|
6
|
+
## 核心原则
|
|
7
|
+
|
|
8
|
+
**一句话记住**:有文件内容变更必须记录,无文件内容变更不需要记录。
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
文件内容变更操作 = beforeEditFile → [执行变更] → afterEditFile → recordSession
|
|
12
|
+
纯对话/只读操作 = 无需调用 beforeEditFile/afterEditFile
|
|
13
|
+
每次对话结束 = recordSession(记录会话)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## MCP 工具概览
|
|
17
|
+
|
|
18
|
+
### 1. recordSession - 会话记录
|
|
19
|
+
- **调用时机**:**每次对话结束时** ← 重要!
|
|
20
|
+
- **用途**:记录 AI 会话的元数据和统计信息
|
|
21
|
+
- **必需参数**:无(sessionId 可选,可自动生成)
|
|
22
|
+
|
|
23
|
+
### 2. beforeEditFile - 文件变更前记录
|
|
24
|
+
- **调用时机**:在执行 Write/Edit/NotebookEdit 等文件变更操作**之前立即调用**
|
|
25
|
+
- **用途**:记录文件的原始状态(快照)
|
|
26
|
+
- **必需参数**:sessionId(与 recordSession 返回的一致), filePaths(绝对路径列表)
|
|
27
|
+
|
|
28
|
+
### 3. afterEditFile - 文件变更后记录
|
|
29
|
+
- **调用时机**:在文件变更操作完成**之后立即调用**
|
|
30
|
+
- **用途**:记录文件的最终状态,计算差异并上传
|
|
31
|
+
- **必需参数**:sessionId(与 beforeEditFile 一致), filePaths(绝对路径列表)
|
|
32
|
+
- **建议参数**:metadata(包含 ide 和 model 信息,如 {"ide": "cursor", "model": "claude-sonnet-4-5"})
|
|
33
|
+
|
|
34
|
+
## 操作分类
|
|
35
|
+
|
|
36
|
+
### ✅ 需要 MCP 记录的操作(文件内容变更)
|
|
37
|
+
- Write, create_file - 创建新文件
|
|
38
|
+
- Edit, search_replace, edit_file - 修改现有文件
|
|
39
|
+
- delete_file - 删除文件
|
|
40
|
+
- NotebookEdit - 编辑 Jupyter Notebook
|
|
41
|
+
- MultiEdit - 多文件编辑
|
|
42
|
+
|
|
43
|
+
### ❌ 不需要 MCP 记录的操作(只读或非文件操作)
|
|
44
|
+
- Read, read_file - 读取文件
|
|
45
|
+
- Glob, Grep, find - 搜索文件或内容
|
|
46
|
+
- Bash, shell - 执行系统命令
|
|
47
|
+
- WebFetch, WebSearch - 网络操作
|
|
48
|
+
- 纯对话 - AI 纯文本回复
|
|
49
|
+
|
|
50
|
+
## 强制要求
|
|
51
|
+
|
|
52
|
+
1. **100% 覆盖**:所有文件内容变更都必须调用 beforeEditFile + afterEditFile
|
|
53
|
+
2. **严格配对**:每次 beforeEditFile 必须有且仅有一次对应的 afterEditFile
|
|
54
|
+
3. **会话一致**:整个对话使用相同的 sessionId
|
|
55
|
+
4. **绝对路径**:filePaths 必须是绝对路径
|
|
56
|
+
5. **即时调用**:beforeEditFile 和 afterEditFile 必须紧邻文件变更操作
|
|
57
|
+
|
|
58
|
+
## 常见错误
|
|
59
|
+
|
|
60
|
+
❌ **错误 1**:只调用 beforeEditFile 不调用 afterEditFile
|
|
61
|
+
❌ **错误 2**:对只读操作(如 Read)也调用 MCP 工具
|
|
62
|
+
❌ **错误 3**:使用相对路径而非绝对路径
|
|
63
|
+
❌ **错误 4**:在不同操作中使用不同的 sessionId
|
|
64
|
+
❌ **错误 5**:合并多次操作到一次 afterEditFile(应批量记录)
|
|
65
|
+
|
|
66
|
+
## 详细文档
|
|
67
|
+
|
|
68
|
+
完整的使用指南请参考:
|
|
69
|
+
- MCP_TOOLS_USAGE_GUIDE.md - 完整的工具使用指南
|
|
70
|
+
- .cursorrules.template - Cursor IDE 的规则模板
|
|
71
|
+
- DESIGN.md - 项目设计文档
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
import os
|
|
75
|
+
import json
|
|
76
|
+
import uuid
|
|
77
|
+
import sys
|
|
78
|
+
from typing import Optional, List
|
|
79
|
+
from datetime import datetime
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
|
|
82
|
+
# 条件导入 fastmcp(需要 Python 3.10+ 且已安装)
|
|
83
|
+
from .compat import MCP_AVAILABLE, HAS_MCP_SUPPORT, FastMCP, get_compatibility_warnings
|
|
84
|
+
|
|
85
|
+
from .client import DevLakeClient
|
|
86
|
+
from .git_utils import get_git_info, get_git_repo_path, get_git_root, get_full_git_context
|
|
87
|
+
from .version_utils import detect_platform_info
|
|
88
|
+
from .logging_config import configure_logging
|
|
89
|
+
from .utils import (
|
|
90
|
+
get_temp_file_path,
|
|
91
|
+
compress_content,
|
|
92
|
+
should_collect_file,
|
|
93
|
+
get_file_type,
|
|
94
|
+
read_file_content
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 创建 MCP 服务器实例(仅在 MCP 可用时)
|
|
98
|
+
# 如果 Python < 3.10 或 fastmcp 未安装,mcp 为 None
|
|
99
|
+
mcp = FastMCP("devlake-mcp") if MCP_AVAILABLE else None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# 工具实现函数(可直接测试)
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
def record_session_impl(
|
|
107
|
+
session_id: Optional[str] = None,
|
|
108
|
+
metadata: Optional[dict] = None
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""
|
|
111
|
+
记录 AI 会话的元数据和统计信息
|
|
112
|
+
|
|
113
|
+
在会话开始时调用,创建会话记录并获取 session_id。
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
# 1. 生成或使用提供的 session_id
|
|
118
|
+
if not session_id:
|
|
119
|
+
session_id = str(uuid.uuid4())
|
|
120
|
+
|
|
121
|
+
# 2. 获取项目路径(优先使用 metadata,否则使用当前目录)
|
|
122
|
+
metadata = metadata or {}
|
|
123
|
+
cwd = metadata.get('project_path') or os.getcwd()
|
|
124
|
+
|
|
125
|
+
# 3. 获取 Git 信息(动态:branch/commit + 静态:author/email)
|
|
126
|
+
git_info = get_git_info(cwd, timeout=1, include_user_info=True)
|
|
127
|
+
git_branch = git_info.get('git_branch', 'unknown')
|
|
128
|
+
git_commit = git_info.get('git_commit', 'unknown')
|
|
129
|
+
git_author = git_info.get('git_author', 'unknown')
|
|
130
|
+
git_email = git_info.get('git_email', 'unknown')
|
|
131
|
+
|
|
132
|
+
# 4. 获取 Git 仓库路径(namespace/name)
|
|
133
|
+
git_repo_path = get_git_repo_path(cwd)
|
|
134
|
+
|
|
135
|
+
# 5. 从 git_repo_path 提取 project_name
|
|
136
|
+
# 例如:yourorg/devlake -> devlake, team/subteam/project -> project
|
|
137
|
+
project_name = git_repo_path.split('/')[-1] if '/' in git_repo_path else git_repo_path
|
|
138
|
+
|
|
139
|
+
# 6. 检测平台信息和版本
|
|
140
|
+
ide_type = metadata.get('ide', 'unknown')
|
|
141
|
+
platform_info = detect_platform_info(ide_type=ide_type)
|
|
142
|
+
|
|
143
|
+
# 7. 构造会话数据
|
|
144
|
+
session_data = {
|
|
145
|
+
'session_id': session_id,
|
|
146
|
+
'user_name': git_author, # 使用 Git 配置的用户名
|
|
147
|
+
'ide_type': ide_type,
|
|
148
|
+
'model_name': metadata.get('model', 'unknown'),
|
|
149
|
+
'git_repo_path': git_repo_path,
|
|
150
|
+
'project_name': project_name,
|
|
151
|
+
'session_start_time': datetime.now().isoformat(),
|
|
152
|
+
'conversation_rounds': 0,
|
|
153
|
+
'is_adopted': 0,
|
|
154
|
+
'git_branch': git_branch,
|
|
155
|
+
'git_commit': git_commit,
|
|
156
|
+
'git_author': git_author,
|
|
157
|
+
'git_email': git_email,
|
|
158
|
+
# 新增:版本信息
|
|
159
|
+
'devlake_mcp_version': platform_info['devlake_mcp_version'],
|
|
160
|
+
'ide_version': platform_info['ide_version'],
|
|
161
|
+
'data_source': 'mcp' # MCP 数据来源(区别于 hook)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# 8. 调用 DevLake API 创建会话(使用 context manager)
|
|
165
|
+
with DevLakeClient() as client:
|
|
166
|
+
response = client.post('/api/ai-coding/sessions', session_data)
|
|
167
|
+
|
|
168
|
+
# 9. 返回结果
|
|
169
|
+
return {
|
|
170
|
+
'success': True,
|
|
171
|
+
'session_id': session_id,
|
|
172
|
+
'timestamp': datetime.now().isoformat(),
|
|
173
|
+
'git_info': {
|
|
174
|
+
'git_repo_path': git_repo_path,
|
|
175
|
+
'project_name': project_name,
|
|
176
|
+
'git_branch': git_branch,
|
|
177
|
+
'git_commit': git_commit[:8] if git_commit != 'unknown' else 'unknown',
|
|
178
|
+
'git_author': git_author,
|
|
179
|
+
'git_email': git_email
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return {
|
|
185
|
+
'success': False,
|
|
186
|
+
'error': str(e),
|
|
187
|
+
'session_id': session_id if session_id else 'unknown'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def before_edit_file_impl(
|
|
192
|
+
session_id: str,
|
|
193
|
+
file_paths: List[str]
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
在文件内容变更操作前调用,记录变更前的文件状态
|
|
197
|
+
|
|
198
|
+
读取文件的当前内容并保存到临时文件,供 afterEditFile 使用。
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
session_id: 会话唯一标识
|
|
202
|
+
file_paths: 即将变更的文件绝对路径列表
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: {
|
|
206
|
+
"success": true,
|
|
207
|
+
"session_id": "session-123",
|
|
208
|
+
"timestamp": "2025-01-07T10:00:00Z",
|
|
209
|
+
"files_snapshot": {
|
|
210
|
+
"/path/to/file1.py": {
|
|
211
|
+
"exists": true,
|
|
212
|
+
"line_count": 100,
|
|
213
|
+
"size": 2048
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
示例:
|
|
219
|
+
>>> before_edit_file("session-123", ["/path/to/file.py"])
|
|
220
|
+
{"success": true, "files_snapshot": {...}}
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
files_snapshot = {}
|
|
224
|
+
|
|
225
|
+
for file_path in file_paths:
|
|
226
|
+
# 1. 转换为绝对路径
|
|
227
|
+
if not os.path.isabs(file_path):
|
|
228
|
+
file_path = os.path.abspath(file_path)
|
|
229
|
+
|
|
230
|
+
# 2. 检查是否应该采集
|
|
231
|
+
if not should_collect_file(file_path):
|
|
232
|
+
files_snapshot[file_path] = {
|
|
233
|
+
'skipped': True,
|
|
234
|
+
'reason': 'Sensitive or binary file'
|
|
235
|
+
}
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# 3. 读取文件内容(如果存在)
|
|
239
|
+
exists = os.path.exists(file_path)
|
|
240
|
+
content = ''
|
|
241
|
+
|
|
242
|
+
if exists:
|
|
243
|
+
try:
|
|
244
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
245
|
+
content = f.read()
|
|
246
|
+
except Exception:
|
|
247
|
+
# 读取失败(如二进制文件),跳过
|
|
248
|
+
files_snapshot[file_path] = {
|
|
249
|
+
'skipped': True,
|
|
250
|
+
'reason': 'Failed to read file (possibly binary)'
|
|
251
|
+
}
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# 4. 保存到临时文件
|
|
255
|
+
temp_file = get_temp_file_path(session_id, file_path)
|
|
256
|
+
try:
|
|
257
|
+
with open(temp_file, 'w', encoding='utf-8') as f:
|
|
258
|
+
data = {
|
|
259
|
+
'file_path': file_path,
|
|
260
|
+
'content': content,
|
|
261
|
+
'timestamp': datetime.now().isoformat()
|
|
262
|
+
}
|
|
263
|
+
json.dump(data, f)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
files_snapshot[file_path] = {
|
|
266
|
+
'skipped': True,
|
|
267
|
+
'reason': f'Failed to save temp file: {str(e)}'
|
|
268
|
+
}
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
# 5. 记录快照信息
|
|
272
|
+
files_snapshot[file_path] = {
|
|
273
|
+
'exists': exists,
|
|
274
|
+
'line_count': len(content.splitlines()) if content else 0,
|
|
275
|
+
'size': len(content.encode('utf-8')) if content else 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
'success': True,
|
|
280
|
+
'session_id': session_id,
|
|
281
|
+
'timestamp': datetime.now().isoformat(),
|
|
282
|
+
'files_snapshot': files_snapshot
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
return {
|
|
287
|
+
'success': False,
|
|
288
|
+
'error': str(e),
|
|
289
|
+
'session_id': session_id
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def after_edit_file_impl(
|
|
294
|
+
session_id: str,
|
|
295
|
+
file_paths: List[str],
|
|
296
|
+
metadata: Optional[dict] = None
|
|
297
|
+
) -> dict:
|
|
298
|
+
"""
|
|
299
|
+
在文件内容变更操作后调用,记录变更后的文件状态
|
|
300
|
+
|
|
301
|
+
读取文件变更后的内容,对比变更前后的差异,并上传到 DevLake API。
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
session_id: 会话唯一标识(与 beforeEditFile 保持一致)
|
|
305
|
+
file_paths: 已变更的文件绝对路径列表
|
|
306
|
+
metadata: 可选的会话元数据,包含 ide 和 model 信息
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
dict: {
|
|
310
|
+
"success": true,
|
|
311
|
+
"session_id": "session-123",
|
|
312
|
+
"timestamp": "2025-01-07T10:01:00Z",
|
|
313
|
+
"uploaded_count": 2,
|
|
314
|
+
"changes": [...]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
示例:
|
|
318
|
+
>>> after_edit_file("session-123", ["/path/to/file.py"], {"ide": "cursor", "model": "claude-sonnet-4-5"})
|
|
319
|
+
{"success": true, "uploaded_count": 1, ...}
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
changes = []
|
|
323
|
+
cwd = os.getcwd()
|
|
324
|
+
|
|
325
|
+
# 获取完整的 Git 上下文(使用统一接口,避免代码重复)
|
|
326
|
+
git_context = get_full_git_context(cwd, use_env_cache=True)
|
|
327
|
+
git_author = git_context['git_author']
|
|
328
|
+
git_email = git_context['git_email']
|
|
329
|
+
git_repo_path = git_context['git_repo_path']
|
|
330
|
+
git_branch = git_context['git_branch']
|
|
331
|
+
git_commit = git_context['git_commit']
|
|
332
|
+
project_name = git_context['project_name']
|
|
333
|
+
git_root = git_context['git_root']
|
|
334
|
+
|
|
335
|
+
# 从 metadata 中获取 ide_type 和 model_name
|
|
336
|
+
metadata = metadata or {}
|
|
337
|
+
ide_type = metadata.get('ide', 'unknown')
|
|
338
|
+
model_name = metadata.get('model', 'unknown')
|
|
339
|
+
|
|
340
|
+
for file_path in file_paths:
|
|
341
|
+
# 1. 转换为绝对路径
|
|
342
|
+
if not os.path.isabs(file_path):
|
|
343
|
+
file_path = os.path.abspath(file_path)
|
|
344
|
+
|
|
345
|
+
# 2. 检查是否应该采集
|
|
346
|
+
if not should_collect_file(file_path):
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# 3. 从临时文件加载 before_content
|
|
350
|
+
temp_file = get_temp_file_path(session_id, file_path)
|
|
351
|
+
before_content = ''
|
|
352
|
+
|
|
353
|
+
if os.path.exists(temp_file):
|
|
354
|
+
try:
|
|
355
|
+
with open(temp_file, 'r', encoding='utf-8') as f:
|
|
356
|
+
data = json.load(f)
|
|
357
|
+
before_content = data.get('content', '')
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
# 4. 读取当前文件内容(after_content)
|
|
362
|
+
after_content = read_file_content(file_path)
|
|
363
|
+
|
|
364
|
+
# 5. 压缩内容(gzip + base64)
|
|
365
|
+
before_content_gz = compress_content(before_content)
|
|
366
|
+
after_content_gz = compress_content(after_content)
|
|
367
|
+
|
|
368
|
+
# 6. 转换文件路径为相对路径(相对于 git root)
|
|
369
|
+
relative_path = file_path
|
|
370
|
+
if git_root:
|
|
371
|
+
try:
|
|
372
|
+
relative_path = os.path.relpath(file_path, git_root)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# 7. 判断变更类型
|
|
377
|
+
change_type = 'create' if not before_content else 'edit'
|
|
378
|
+
|
|
379
|
+
# 8. 构造变更数据
|
|
380
|
+
change_data = {
|
|
381
|
+
'session_id': session_id,
|
|
382
|
+
'user_name': git_author,
|
|
383
|
+
'ide_type': ide_type,
|
|
384
|
+
'model_name': model_name,
|
|
385
|
+
'git_repo_path': git_repo_path,
|
|
386
|
+
'project_name': project_name,
|
|
387
|
+
'file_path': relative_path,
|
|
388
|
+
'file_type': get_file_type(file_path),
|
|
389
|
+
'change_type': change_type,
|
|
390
|
+
'tool_name': 'MCP', # MCP 工具标识
|
|
391
|
+
'before_content_gz': before_content_gz,
|
|
392
|
+
'after_content_gz': after_content_gz,
|
|
393
|
+
'git_branch': git_branch,
|
|
394
|
+
'git_commit': git_commit,
|
|
395
|
+
'git_author': git_author,
|
|
396
|
+
'git_email': git_email,
|
|
397
|
+
'change_time': datetime.now().isoformat(),
|
|
398
|
+
'cwd': cwd
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
changes.append(change_data)
|
|
402
|
+
|
|
403
|
+
# 9. 清理临时文件
|
|
404
|
+
if os.path.exists(temp_file):
|
|
405
|
+
try:
|
|
406
|
+
os.remove(temp_file)
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
# 10. 批量上传到 DevLake API(使用 context manager)
|
|
411
|
+
if changes:
|
|
412
|
+
with DevLakeClient() as client:
|
|
413
|
+
response = client.post('/api/ai-coding/file-changes', {'changes': changes})
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
'success': True,
|
|
417
|
+
'session_id': session_id,
|
|
418
|
+
'timestamp': datetime.now().isoformat(),
|
|
419
|
+
'uploaded_count': len(changes),
|
|
420
|
+
'changes': [
|
|
421
|
+
{
|
|
422
|
+
'file_path': c['file_path'],
|
|
423
|
+
'change_type': c['change_type'],
|
|
424
|
+
'file_type': c['file_type']
|
|
425
|
+
}
|
|
426
|
+
for c in changes
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
return {
|
|
432
|
+
'success': False,
|
|
433
|
+
'error': str(e),
|
|
434
|
+
'session_id': session_id
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ============================================================================
|
|
439
|
+
# MCP 工具装饰器(包装实现函数)
|
|
440
|
+
# 注意:这些装饰器仅在 MCP 可用时才会生效
|
|
441
|
+
# ============================================================================
|
|
442
|
+
|
|
443
|
+
# 仅在 MCP 可用时注册工具
|
|
444
|
+
if mcp is not None:
|
|
445
|
+
@mcp.tool
|
|
446
|
+
def record_session(
|
|
447
|
+
session_id: str,
|
|
448
|
+
metadata: dict
|
|
449
|
+
) -> dict:
|
|
450
|
+
"""
|
|
451
|
+
记录 AI 会话的元数据和统计信息
|
|
452
|
+
|
|
453
|
+
## 调用时机
|
|
454
|
+
|
|
455
|
+
✅ **每次对话结束时** ← 重要!必须调用
|
|
456
|
+
|
|
457
|
+
## 用途
|
|
458
|
+
|
|
459
|
+
记录 AI 会话的元数据和统计信息,包括:
|
|
460
|
+
- 用户消息
|
|
461
|
+
- AI回复
|
|
462
|
+
- 使用的模型和 IDE
|
|
463
|
+
|
|
464
|
+
## 参数
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
session_id: 会话 ID(必填,示例:1c1a4b88-8701-4dc0-b53f-9d5262ac6628,整个对话开始时应确定一个统一的sessionId,并在所有后续对话轮次操作中保持该ID不变)
|
|
468
|
+
metadata: 会话元数据(必填),支持字段:
|
|
469
|
+
- prompt_content: 用户消息(如 "实现用户登录功能")
|
|
470
|
+
- response_content: AI回复
|
|
471
|
+
- model: 模型名称(如 "claude-sonnet-4-5")
|
|
472
|
+
- ide: IDE 类型(如 "cursor", "claude-code")
|
|
473
|
+
|
|
474
|
+
## 返回值
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
dict: {
|
|
478
|
+
"success": true,
|
|
479
|
+
"session_id": "1c1a4b88-8701-4dc0-b53f-9d5262ac6628",
|
|
480
|
+
"timestamp": "2025-01-07T10:00:00Z",
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
## 注意事项
|
|
484
|
+
|
|
485
|
+
- ✅ **每次对话结束时必须调用**
|
|
486
|
+
- ✅ 文件变更场景:sessionId 必须与 beforeEditFile/afterEditFile 保持一致
|
|
487
|
+
- ✅ 必须提供 sessionId 和 metadata
|
|
488
|
+
- ❌ 不要在对话过程中多次调用
|
|
489
|
+
"""
|
|
490
|
+
return record_session_impl(session_id, metadata)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@mcp.tool
|
|
494
|
+
def before_edit_file(
|
|
495
|
+
session_id: str,
|
|
496
|
+
file_paths: List[str]
|
|
497
|
+
) -> dict:
|
|
498
|
+
"""
|
|
499
|
+
在文件内容变更操作前调用,记录变更前的文件状态
|
|
500
|
+
|
|
501
|
+
## 调用时机
|
|
502
|
+
|
|
503
|
+
✅ **文件内容变更操作前**:在执行以下操作**之前立即调用**
|
|
504
|
+
- Write, create_file - 创建新文件
|
|
505
|
+
- Edit, search_replace, edit_file - 修改现有文件
|
|
506
|
+
- delete_file - 删除文件
|
|
507
|
+
- NotebookEdit - 编辑 Jupyter Notebook
|
|
508
|
+
|
|
509
|
+
❌ **不要在以下操作前调用**:
|
|
510
|
+
- Read, read_file - 读取文件(只读操作)
|
|
511
|
+
- Glob, Grep - 搜索文件(只读操作)
|
|
512
|
+
- Bash, shell - 执行命令(非文件操作)
|
|
513
|
+
|
|
514
|
+
## 用途
|
|
515
|
+
|
|
516
|
+
记录文件的原始状态(快照),保存到临时文件,供 afterEditFile 对比使用。
|
|
517
|
+
必须与 afterEditFile 成对出现。
|
|
518
|
+
|
|
519
|
+
## 参数
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
session_id: 会话 ID(必填,示例:1c1a4b88-8701-4dc0-b53f-9d5262ac6628,整个对话开始时应确定一个统一的sessionId,并在所有后续对话轮次操作中保持该ID不变)
|
|
523
|
+
file_paths: 即将变更的文件**绝对路径**列表
|
|
524
|
+
⚠️ 必须使用绝对路径,如 ["/home/user/project/src/main.py", "/home/user/project/src/utils.py"]
|
|
525
|
+
❌ 不要使用相对路径,如 ["src/main.py"]
|
|
526
|
+
|
|
527
|
+
## 返回值
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
dict: {
|
|
531
|
+
"success": true,
|
|
532
|
+
"session_id": "session-123",
|
|
533
|
+
"timestamp": "2025-01-07T10:00:00Z",
|
|
534
|
+
"files_snapshot": {
|
|
535
|
+
"/absolute/path/to/file1.py": {
|
|
536
|
+
"exists": true,
|
|
537
|
+
"line_count": 100,
|
|
538
|
+
"size": 2048
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## 注意事项
|
|
545
|
+
- ✅ 必须在文件变更操作**之前**调用
|
|
546
|
+
- ✅ 必须使用绝对路径
|
|
547
|
+
- ❌ 不要对只读操作调用
|
|
548
|
+
- ❌ 不要使用相对路径
|
|
549
|
+
"""
|
|
550
|
+
return before_edit_file_impl(session_id, file_paths)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@mcp.tool
|
|
554
|
+
def after_edit_file(
|
|
555
|
+
session_id: str,
|
|
556
|
+
file_paths: List[str],
|
|
557
|
+
metadata: dict = None
|
|
558
|
+
) -> dict:
|
|
559
|
+
"""
|
|
560
|
+
在文件内容变更操作后调用,记录变更后的文件状态
|
|
561
|
+
|
|
562
|
+
## 调用时机
|
|
563
|
+
|
|
564
|
+
✅ **文件内容变更操作后**:在文件变更完成**之后立即调用**
|
|
565
|
+
|
|
566
|
+
必须在对应的 beforeEditFile 调用之后执行,两者必须成对出现。
|
|
567
|
+
|
|
568
|
+
## 用途
|
|
569
|
+
|
|
570
|
+
1. 读取文件变更后的内容
|
|
571
|
+
2. 从临时文件加载变更前的内容
|
|
572
|
+
3. 计算变更差异(在服务端进行)
|
|
573
|
+
4. 上传到 DevLake API
|
|
574
|
+
5. 清理临时文件
|
|
575
|
+
|
|
576
|
+
## 参数
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
session_id: 会话唯一标识(⚠️ 必须与 beforeEditFile 的 sessionId 一致)
|
|
580
|
+
file_paths: 已变更的文件**绝对路径**列表
|
|
581
|
+
⚠️ 必须与 beforeEditFile 的 filePaths 完全一致
|
|
582
|
+
⚠️ 必须使用绝对路径
|
|
583
|
+
metadata: 会话元数据(可选),支持字段:
|
|
584
|
+
- ide: IDE 类型(如 "cursor", "claude-code")
|
|
585
|
+
- model: 模型名称(如 "claude-sonnet-4-5")
|
|
586
|
+
|
|
587
|
+
## 返回值
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
dict: {
|
|
591
|
+
"success": true,
|
|
592
|
+
"session_id": "session-123",
|
|
593
|
+
"timestamp": "2025-01-07T10:01:00Z",
|
|
594
|
+
"uploaded_count": 2,
|
|
595
|
+
"changes": [
|
|
596
|
+
{
|
|
597
|
+
"file_path": "src/main.py", # 相对路径(相对于 git root)
|
|
598
|
+
"change_type": "edit", # "create", "edit", "delete"
|
|
599
|
+
"file_type": "py"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"file_path": "src/utils.js",
|
|
603
|
+
"change_type": "create",
|
|
604
|
+
"file_type": "js"
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
## 注意事项
|
|
611
|
+
|
|
612
|
+
- ✅ 必须在文件变更操作**之后**调用
|
|
613
|
+
- ✅ 必须与 beforeEditFile 成对出现
|
|
614
|
+
- ✅ sessionId 必须与 beforeEditFile 一致
|
|
615
|
+
- ✅ filePaths 必须与 beforeEditFile 完全一致
|
|
616
|
+
- ✅ 必须使用绝对路径
|
|
617
|
+
- ✅ **建议传递 metadata**(包含 ide 和 model 信息)
|
|
618
|
+
- ❌ 不要在 beforeEditFile 之前调用
|
|
619
|
+
- ❌ 不要使用不同的 sessionId
|
|
620
|
+
- ❌ 不要使用不同的 filePaths
|
|
621
|
+
"""
|
|
622
|
+
return after_edit_file_impl(session_id, file_paths, metadata)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def main():
|
|
626
|
+
"""
|
|
627
|
+
启动 MCP 服务器
|
|
628
|
+
|
|
629
|
+
使用 stdio 传输协议,适合与 Claude Desktop 等客户端集成。
|
|
630
|
+
|
|
631
|
+
注意:MCP Server 需要 Python 3.10+ 和 fastmcp 包。
|
|
632
|
+
"""
|
|
633
|
+
# 检查 MCP 是否可用
|
|
634
|
+
if not MCP_AVAILABLE:
|
|
635
|
+
print("\n" + "=" * 60, file=sys.stderr)
|
|
636
|
+
print("DevLake MCP Server - 启动失败", file=sys.stderr)
|
|
637
|
+
print("=" * 60, file=sys.stderr)
|
|
638
|
+
|
|
639
|
+
# 显示警告信息
|
|
640
|
+
warnings = get_compatibility_warnings()
|
|
641
|
+
for warning in warnings:
|
|
642
|
+
print(warning, file=sys.stderr)
|
|
643
|
+
|
|
644
|
+
print("\n可用功能:", file=sys.stderr)
|
|
645
|
+
print(" - Hooks 模式: ✓ 可用(适用于 Claude Code/Cursor hooks)", file=sys.stderr)
|
|
646
|
+
print(" - MCP Server: ✗ 不可用", file=sys.stderr)
|
|
647
|
+
|
|
648
|
+
if not HAS_MCP_SUPPORT:
|
|
649
|
+
print("\n推荐操作: 升级到 Python 3.10+ 以使用 MCP Server", file=sys.stderr)
|
|
650
|
+
else:
|
|
651
|
+
print("\n推荐操作: pip install 'devlake-mcp[mcp]'", file=sys.stderr)
|
|
652
|
+
|
|
653
|
+
print("=" * 60, file=sys.stderr)
|
|
654
|
+
sys.exit(1)
|
|
655
|
+
|
|
656
|
+
# 配置日志(读取环境变量)
|
|
657
|
+
configure_logging()
|
|
658
|
+
|
|
659
|
+
print("✓ MCP Server 启动成功 (FastMCP)", file=sys.stderr)
|
|
660
|
+
mcp.run() # 默认使用 stdio transport
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
if __name__ == "__main__":
|
|
664
|
+
main()
|