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/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()