aicodestat 0.0.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.
- aicodestat-0.0.1.dist-info/METADATA +110 -0
- aicodestat-0.0.1.dist-info/RECORD +34 -0
- aicodestat-0.0.1.dist-info/WHEEL +5 -0
- aicodestat-0.0.1.dist-info/entry_points.txt +5 -0
- aicodestat-0.0.1.dist-info/top_level.txt +10 -0
- cli/__init__.py +2 -0
- cli/exporter.py +111 -0
- cli/main.py +213 -0
- cli/menus.py +540 -0
- cli/views.py +277 -0
- compute/__init__.py +2 -0
- compute/cache.py +90 -0
- compute/diff_engine.py +69 -0
- compute/lcs_engine.py +73 -0
- compute/metrics_service.py +362 -0
- config.py +120 -0
- local_mcp_server.py +260 -0
- logging_config.py +68 -0
- main.py +164 -0
- mcp/__init__.py +2 -0
- mcp/agent_adapter.py +69 -0
- mcp/api_schemas.py +26 -0
- mcp/routes_after.py +121 -0
- mcp/routes_before.py +68 -0
- mcp/routes_tools.py +100 -0
- service_manager.py +221 -0
- storage/__init__.py +2 -0
- storage/backup.py +185 -0
- storage/db.py +156 -0
- storage/models.py +338 -0
- storage/scheduler.py +111 -0
- utils/__init__.py +2 -0
- utils/port_utils.py +59 -0
- utils/time_utils.py +37 -0
local_mcp_server.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""应用入口,使用 FastMCP 框架实现 MCP Server,通过 HTTP 后台服务暴露接口"""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Any, Dict
|
|
8
|
+
from config import get_server_config, get_database_config
|
|
9
|
+
from logging_config import setup_logging
|
|
10
|
+
from storage.db import get_db
|
|
11
|
+
from storage.scheduler import start_scheduler, stop_scheduler
|
|
12
|
+
from storage.models import (
|
|
13
|
+
save_before_edit,
|
|
14
|
+
get_before_edit,
|
|
15
|
+
delete_before_edit,
|
|
16
|
+
save_session_summary,
|
|
17
|
+
save_code_diff_lines,
|
|
18
|
+
)
|
|
19
|
+
from compute.diff_engine import extract_diff_lines
|
|
20
|
+
from utils.port_utils import find_available_port
|
|
21
|
+
|
|
22
|
+
# 设置日志(初始化为INFO级别,main函数中会根据配置调整)
|
|
23
|
+
logger = setup_logging(log_level="INFO", module_name="mcp_server")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _import_fastmcp():
|
|
27
|
+
"""Import FastMCP from the official MCP SDK, avoiding local mcp/ shadowing."""
|
|
28
|
+
original_sys_path = list(sys.path)
|
|
29
|
+
try:
|
|
30
|
+
project_root = os.path.abspath(os.path.dirname(__file__))
|
|
31
|
+
sys.path = [p for p in sys.path if p not in ("", ".", project_root)]
|
|
32
|
+
from mcp.server.fastmcp import FastMCP # type: ignore
|
|
33
|
+
return FastMCP
|
|
34
|
+
finally:
|
|
35
|
+
sys.path = original_sys_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_mcp_app(host: str = "127.0.0.1", port: int = 8000):
|
|
39
|
+
"""创建 FastMCP 应用并注册工具"""
|
|
40
|
+
FastMCP = _import_fastmcp()
|
|
41
|
+
# FastMCP 构造函数接受 host, port, streamable_http_path 等参数
|
|
42
|
+
# streamable_http_path 默认为 '/mcp',但 Cursor 可能请求根路径,所以设置为 '/'
|
|
43
|
+
mcp_app = FastMCP(
|
|
44
|
+
"local-code-stat",
|
|
45
|
+
host=host,
|
|
46
|
+
port=port,
|
|
47
|
+
streamable_http_path="/" # 设置为根路径,兼容 Cursor 的请求
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@mcp_app.tool()
|
|
51
|
+
def RecordBeforeEdit(session_id: str, file_path: str, code_before: str) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Record the complete code content before file editing (temporary local storage, will be cleaned up automatically, no redundant retention)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
session_id: Current session ID, generated by Agent, must match RecordAfterEdit's session_id
|
|
57
|
+
file_path: Absolute path of the target file
|
|
58
|
+
code_before: Complete code content before editing (preserve original format, blank lines, ensure accurate line numbers)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary containing status and message
|
|
62
|
+
"""
|
|
63
|
+
if not session_id or not session_id.strip():
|
|
64
|
+
return {"status": "error", "message": "session_id cannot be empty"}
|
|
65
|
+
if not file_path or not file_path.strip():
|
|
66
|
+
return {"status": "error", "message": "file_path cannot be empty"}
|
|
67
|
+
if code_before is None or code_before == "":
|
|
68
|
+
return {"status": "error", "message": "code_before cannot be empty"}
|
|
69
|
+
|
|
70
|
+
success = save_before_edit(session_id=session_id, file_path=file_path, code_before=code_before)
|
|
71
|
+
if not success:
|
|
72
|
+
return {"status": "error", "message": "Failed to save code before editing"}
|
|
73
|
+
|
|
74
|
+
logger.info(f"RecordBeforeEdit: session={session_id}, file={file_path}")
|
|
75
|
+
return {"status": "success", "message": "Code before editing recorded successfully"}
|
|
76
|
+
|
|
77
|
+
@mcp_app.tool()
|
|
78
|
+
def RecordAfterEdit(
|
|
79
|
+
session_id: str,
|
|
80
|
+
file_path: str,
|
|
81
|
+
code_after: str,
|
|
82
|
+
session_info: Optional[str] = None,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Record the complete code content after file editing, extract specific diff lines (added/modified) with line numbers, clean up temporary data, and retain only diff information
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
session_id: Current session ID, must match RecordBeforeEdit's session_id to associate the same editing operation
|
|
89
|
+
file_path: Absolute path of the target file, must match RecordBeforeEdit's file_path
|
|
90
|
+
code_after: Complete code content after editing (preserve original format, blank lines, ensure accurate line numbers)
|
|
91
|
+
session_info: Optional session supplementary information (e.g., user instructions, Agent type, operation time) for subsequent statistical filtering
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary containing status, message, and data
|
|
95
|
+
"""
|
|
96
|
+
if not session_id or not session_id.strip():
|
|
97
|
+
return {"status": "error", "message": "session_id cannot be empty"}
|
|
98
|
+
if not file_path or not file_path.strip():
|
|
99
|
+
return {"status": "error", "message": "file_path cannot be empty"}
|
|
100
|
+
if code_after is None or code_after == "":
|
|
101
|
+
return {"status": "error", "message": "code_after cannot be empty"}
|
|
102
|
+
|
|
103
|
+
code_before = get_before_edit(session_id, file_path)
|
|
104
|
+
if code_before is None:
|
|
105
|
+
return {
|
|
106
|
+
"status": "error",
|
|
107
|
+
"message": (
|
|
108
|
+
f"Code record before editing not found, please call RecordBeforeEdit Tool first "
|
|
109
|
+
f"(session_id={session_id}, file_path={file_path})"
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
diff_lines = extract_diff_lines(code_before, code_after)
|
|
114
|
+
add_lines_count = sum(1 for d in diff_lines if d["diff_type"] == "add")
|
|
115
|
+
modify_lines_count = sum(1 for d in diff_lines if d["diff_type"] == "modify")
|
|
116
|
+
total_lines_after = len(code_after.split("\n"))
|
|
117
|
+
|
|
118
|
+
success = save_session_summary(
|
|
119
|
+
session_id=session_id,
|
|
120
|
+
file_path=file_path,
|
|
121
|
+
add_lines_count=add_lines_count,
|
|
122
|
+
modify_lines_count=modify_lines_count,
|
|
123
|
+
total_lines_after=total_lines_after,
|
|
124
|
+
session_info=session_info,
|
|
125
|
+
)
|
|
126
|
+
if not success:
|
|
127
|
+
return {"status": "error", "message": "Failed to save session summary"}
|
|
128
|
+
|
|
129
|
+
if diff_lines:
|
|
130
|
+
ok = save_code_diff_lines(session_id=session_id, file_path=file_path, diff_lines=diff_lines)
|
|
131
|
+
if not ok:
|
|
132
|
+
logger.warning("Failed to save diff lines details, but session summary has been saved")
|
|
133
|
+
|
|
134
|
+
delete_before_edit(session_id, file_path)
|
|
135
|
+
|
|
136
|
+
logger.info(
|
|
137
|
+
f"RecordAfterEdit: session={session_id}, file={file_path}, "
|
|
138
|
+
f"add={add_lines_count}, modify={modify_lines_count}, total_diff={len(diff_lines)}"
|
|
139
|
+
)
|
|
140
|
+
return {
|
|
141
|
+
"status": "success",
|
|
142
|
+
"message": "Code after editing recorded successfully, diff lines extracted and stored",
|
|
143
|
+
"data": {
|
|
144
|
+
"add_lines_count": add_lines_count,
|
|
145
|
+
"modify_lines_count": modify_lines_count,
|
|
146
|
+
"total_diff_lines": len(diff_lines),
|
|
147
|
+
"total_lines_after": total_lines_after,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return mcp_app
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
"""主函数:启动 FastMCP HTTP 服务器"""
|
|
156
|
+
parser = argparse.ArgumentParser(description="本地MCP Server AI代码统计系统(基于FastMCP)")
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"command",
|
|
159
|
+
choices=["start"],
|
|
160
|
+
help="启动服务器"
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--host",
|
|
164
|
+
type=str,
|
|
165
|
+
help="服务器监听地址(默认从config.json读取)"
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--port",
|
|
169
|
+
type=int,
|
|
170
|
+
help="服务器监听端口(默认从config.json读取)"
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--daemon",
|
|
174
|
+
action="store_true",
|
|
175
|
+
help="后台运行(Windows下使用简单后台进程,Unix下建议使用nohup或systemd)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
if args.command == "start":
|
|
181
|
+
# 初始化数据库和定时任务
|
|
182
|
+
try:
|
|
183
|
+
db = get_db()
|
|
184
|
+
db.initialize()
|
|
185
|
+
logger.info("Database initialized successfully")
|
|
186
|
+
|
|
187
|
+
start_scheduler()
|
|
188
|
+
logger.info("Scheduler started (auto backup and cleanup)")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f"Failed to initialize: {e}", exc_info=True)
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
# 获取配置
|
|
194
|
+
server_config = get_server_config()
|
|
195
|
+
host = args.host or server_config["host"]
|
|
196
|
+
preferred_port = args.port or server_config["port"]
|
|
197
|
+
log_level = server_config.get("log_level", "info")
|
|
198
|
+
log_file = server_config.get("log_file")
|
|
199
|
+
|
|
200
|
+
# 检查端口是否可用,如果被占用则选择随机端口
|
|
201
|
+
try:
|
|
202
|
+
port = find_available_port(host, preferred_port)
|
|
203
|
+
if port != preferred_port:
|
|
204
|
+
logger.warning(f"Port {preferred_port} is in use, using port {port} instead")
|
|
205
|
+
except RuntimeError as e:
|
|
206
|
+
logger.error(f"Failed to find available port: {e}")
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
|
|
209
|
+
# 设置日志(如果配置了日志文件,添加文件handler)
|
|
210
|
+
if log_file:
|
|
211
|
+
# 添加文件handler到现有logger
|
|
212
|
+
from pathlib import Path
|
|
213
|
+
log_path = Path(log_file)
|
|
214
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
216
|
+
file_handler.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
217
|
+
formatter = logging.Formatter(
|
|
218
|
+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
219
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
220
|
+
)
|
|
221
|
+
file_handler.setFormatter(formatter)
|
|
222
|
+
logger.addHandler(file_handler)
|
|
223
|
+
logger.info(f"Logging to file: {log_file}")
|
|
224
|
+
|
|
225
|
+
# 设置日志级别
|
|
226
|
+
logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
227
|
+
|
|
228
|
+
# 创建 FastMCP 应用(传入 host 和 port)
|
|
229
|
+
mcp_app = create_mcp_app(host=host, port=port)
|
|
230
|
+
|
|
231
|
+
logger.info(f"Starting FastMCP Server on {host}:{port} (transport: streamable-http)")
|
|
232
|
+
logger.info("MCP Tools available:")
|
|
233
|
+
logger.info(" - RecordBeforeEdit: Record code before file editing")
|
|
234
|
+
logger.info(" - RecordAfterEdit: Record code after file editing and extract diff lines")
|
|
235
|
+
|
|
236
|
+
if args.daemon:
|
|
237
|
+
# 后台运行(简单实现,生产环境建议使用systemd/supervisor等)
|
|
238
|
+
logger.warning("Daemon mode is a simple implementation. For production, use systemd/supervisor/nohup.")
|
|
239
|
+
import multiprocessing
|
|
240
|
+
|
|
241
|
+
def run_server():
|
|
242
|
+
mcp_app.run(transport="streamable-http")
|
|
243
|
+
|
|
244
|
+
process = multiprocessing.Process(target=run_server, daemon=True)
|
|
245
|
+
process.start()
|
|
246
|
+
logger.info(f"Server started in background (PID: {process.pid})")
|
|
247
|
+
logger.info("To stop the server, use: kill <PID>")
|
|
248
|
+
else:
|
|
249
|
+
# 前台运行 FastMCP HTTP 服务器
|
|
250
|
+
try:
|
|
251
|
+
mcp_app.run(transport="streamable-http")
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
logger.info("Server stopped by user")
|
|
254
|
+
stop_scheduler()
|
|
255
|
+
logger.info("Scheduler stopped")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
260
|
+
|
logging_config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""统一日志配置模块"""
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logging(
|
|
9
|
+
log_level: str = "INFO",
|
|
10
|
+
log_file: Optional[str] = None,
|
|
11
|
+
module_name: Optional[str] = None,
|
|
12
|
+
stream: Optional[object] = None
|
|
13
|
+
) -> logging.Logger:
|
|
14
|
+
"""
|
|
15
|
+
设置日志配置
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
log_level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
19
|
+
log_file: 日志文件路径(可选)
|
|
20
|
+
module_name: 模块名称(用于logger命名)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
配置好的logger实例
|
|
24
|
+
"""
|
|
25
|
+
# 转换日志级别
|
|
26
|
+
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
|
27
|
+
|
|
28
|
+
# 创建formatter
|
|
29
|
+
formatter = logging.Formatter(
|
|
30
|
+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
31
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# 创建logger
|
|
35
|
+
logger_name = module_name if module_name else __name__
|
|
36
|
+
logger = logging.getLogger(logger_name)
|
|
37
|
+
logger.setLevel(numeric_level)
|
|
38
|
+
|
|
39
|
+
# 清除已有的handlers
|
|
40
|
+
logger.handlers.clear()
|
|
41
|
+
|
|
42
|
+
# 控制台handler
|
|
43
|
+
# IMPORTANT:
|
|
44
|
+
# - MCP stdio mode uses stdout for JSON-RPC. Writing logs to stdout will break the protocol.
|
|
45
|
+
# - Default to stderr to be safe for both CLI and MCP.
|
|
46
|
+
if stream is None:
|
|
47
|
+
stream = sys.stderr
|
|
48
|
+
console_handler = logging.StreamHandler(stream)
|
|
49
|
+
console_handler.setLevel(numeric_level)
|
|
50
|
+
console_handler.setFormatter(formatter)
|
|
51
|
+
logger.addHandler(console_handler)
|
|
52
|
+
|
|
53
|
+
# 文件handler(如果指定)
|
|
54
|
+
if log_file:
|
|
55
|
+
log_path = Path(log_file)
|
|
56
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
58
|
+
file_handler.setLevel(numeric_level)
|
|
59
|
+
file_handler.setFormatter(formatter)
|
|
60
|
+
logger.addHandler(file_handler)
|
|
61
|
+
|
|
62
|
+
return logger
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_logger(name: str) -> logging.Logger:
|
|
66
|
+
"""获取指定名称的logger"""
|
|
67
|
+
return logging.getLogger(name)
|
|
68
|
+
|
main.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""统一入口:支持启动服务器或CLI"""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""主入口函数"""
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="本地MCP Server AI代码统计系统",
|
|
10
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
11
|
+
epilog="""
|
|
12
|
+
示例:
|
|
13
|
+
%(prog)s server start # 启动MCP服务器
|
|
14
|
+
%(prog)s server start --port 8080 # 指定端口启动服务器
|
|
15
|
+
%(prog)s cli # 启动交互式CLI
|
|
16
|
+
%(prog)s cli --session <id> # 快捷查询会话
|
|
17
|
+
%(prog)s cli --file <path> # 快捷查询文件
|
|
18
|
+
"""
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
|
22
|
+
|
|
23
|
+
# Server子命令
|
|
24
|
+
server_parser = subparsers.add_parser('server', help='启动MCP服务器')
|
|
25
|
+
server_parser.add_argument(
|
|
26
|
+
'action',
|
|
27
|
+
choices=['start'],
|
|
28
|
+
help='启动服务器'
|
|
29
|
+
)
|
|
30
|
+
server_parser.add_argument(
|
|
31
|
+
'--host',
|
|
32
|
+
type=str,
|
|
33
|
+
help='服务器监听地址(默认从config.json读取)'
|
|
34
|
+
)
|
|
35
|
+
server_parser.add_argument(
|
|
36
|
+
'--port',
|
|
37
|
+
type=int,
|
|
38
|
+
help='服务器监听端口(默认从config.json读取)'
|
|
39
|
+
)
|
|
40
|
+
server_parser.add_argument(
|
|
41
|
+
'--daemon',
|
|
42
|
+
action='store_true',
|
|
43
|
+
help='后台运行'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# CLI子命令
|
|
47
|
+
cli_parser = subparsers.add_parser('cli', help='启动交互式CLI')
|
|
48
|
+
cli_parser.add_argument(
|
|
49
|
+
'--session',
|
|
50
|
+
type=str,
|
|
51
|
+
help='按会话ID查询(快捷命令)'
|
|
52
|
+
)
|
|
53
|
+
cli_parser.add_argument(
|
|
54
|
+
'--file',
|
|
55
|
+
type=str,
|
|
56
|
+
help='按文件路径查询(快捷命令)'
|
|
57
|
+
)
|
|
58
|
+
cli_parser.add_argument(
|
|
59
|
+
'--project',
|
|
60
|
+
type=str,
|
|
61
|
+
help='按项目根目录查询(快捷命令)'
|
|
62
|
+
)
|
|
63
|
+
cli_parser.add_argument(
|
|
64
|
+
'--export',
|
|
65
|
+
type=str,
|
|
66
|
+
choices=['json', 'csv'],
|
|
67
|
+
help='导出格式(需配合--session/--file/--project使用)'
|
|
68
|
+
)
|
|
69
|
+
cli_parser.add_argument(
|
|
70
|
+
'--output',
|
|
71
|
+
type=str,
|
|
72
|
+
help='导出文件路径(需配合--export使用)'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
if not args.command:
|
|
78
|
+
parser.print_help()
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
if args.command == 'server':
|
|
82
|
+
# 启动服务器
|
|
83
|
+
if args.action == 'start':
|
|
84
|
+
import uvicorn
|
|
85
|
+
import logging
|
|
86
|
+
from config import get_server_config
|
|
87
|
+
from logging_config import setup_logging
|
|
88
|
+
from storage.db import get_db
|
|
89
|
+
from storage.scheduler import start_scheduler
|
|
90
|
+
from local_mcp_server import app
|
|
91
|
+
|
|
92
|
+
logger = setup_logging(log_level="INFO", module_name="mcp_server")
|
|
93
|
+
|
|
94
|
+
# 初始化数据库
|
|
95
|
+
try:
|
|
96
|
+
db = get_db()
|
|
97
|
+
db.initialize()
|
|
98
|
+
logger.info("Database initialized successfully")
|
|
99
|
+
start_scheduler()
|
|
100
|
+
logger.info("Scheduler started (auto backup and cleanup)")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Failed to initialize: {e}")
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
# 获取配置
|
|
106
|
+
server_config = get_server_config()
|
|
107
|
+
host = args.host or server_config["host"]
|
|
108
|
+
preferred_port = args.port or server_config["port"]
|
|
109
|
+
log_level = server_config.get("log_level", "info")
|
|
110
|
+
|
|
111
|
+
# 检查端口是否可用,如果被占用则选择随机端口
|
|
112
|
+
from utils.port_utils import find_available_port
|
|
113
|
+
try:
|
|
114
|
+
port = find_available_port(host, preferred_port)
|
|
115
|
+
if port != preferred_port:
|
|
116
|
+
logger.warning(f"Port {preferred_port} is in use, using port {port} instead")
|
|
117
|
+
except RuntimeError as e:
|
|
118
|
+
logger.error(f"Failed to find available port: {e}")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
122
|
+
logger.info(f"Starting MCP Server on {host}:{port}")
|
|
123
|
+
logger.info("MCP Tools available at:")
|
|
124
|
+
logger.info(f" GET http://{host}:{port}/mcp/tools (Tool discovery)")
|
|
125
|
+
logger.info(f" POST http://{host}:{port}/mcp/record_before")
|
|
126
|
+
logger.info(f" POST http://{host}:{port}/mcp/record_after")
|
|
127
|
+
|
|
128
|
+
if args.daemon:
|
|
129
|
+
logger.warning("Daemon mode is a simple implementation. For production, use systemd/supervisor/nohup.")
|
|
130
|
+
import multiprocessing
|
|
131
|
+
|
|
132
|
+
def run_server():
|
|
133
|
+
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
|
134
|
+
|
|
135
|
+
process = multiprocessing.Process(target=run_server, daemon=True)
|
|
136
|
+
process.start()
|
|
137
|
+
logger.info(f"Server started in background (PID: {process.pid})")
|
|
138
|
+
logger.info("To stop the server, use: kill <PID>")
|
|
139
|
+
else:
|
|
140
|
+
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
|
141
|
+
|
|
142
|
+
elif args.command == 'cli':
|
|
143
|
+
# 启动CLI
|
|
144
|
+
from cli.main import main as cli_main
|
|
145
|
+
# 设置快捷命令参数
|
|
146
|
+
if args.session or args.file or args.project:
|
|
147
|
+
# 快捷命令模式,需要重新设置sys.argv
|
|
148
|
+
sys.argv = ['cli/main.py']
|
|
149
|
+
if args.session:
|
|
150
|
+
sys.argv.extend(['--session', args.session])
|
|
151
|
+
if args.file:
|
|
152
|
+
sys.argv.extend(['--file', args.file])
|
|
153
|
+
if args.project:
|
|
154
|
+
sys.argv.extend(['--project', args.project])
|
|
155
|
+
if args.export:
|
|
156
|
+
sys.argv.extend(['--export', args.export])
|
|
157
|
+
if args.output:
|
|
158
|
+
sys.argv.extend(['--output', args.output])
|
|
159
|
+
cli_main()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == '__main__':
|
|
163
|
+
main()
|
|
164
|
+
|
mcp/__init__.py
ADDED
mcp/agent_adapter.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""可选,封装不同Agent字段名差异的映射"""
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
|
|
4
|
+
# Agent参数映射表
|
|
5
|
+
AGENT_FIELD_MAPPINGS = {
|
|
6
|
+
"cursor": {
|
|
7
|
+
"file_path": "file_path",
|
|
8
|
+
"target_file": "file_path", # 别名
|
|
9
|
+
},
|
|
10
|
+
"claude": {
|
|
11
|
+
"file_path": "file_path",
|
|
12
|
+
"target_file": "file_path",
|
|
13
|
+
},
|
|
14
|
+
"trea": {
|
|
15
|
+
"file_path": "file_path",
|
|
16
|
+
},
|
|
17
|
+
"qoder": {
|
|
18
|
+
"file_path": "file_path",
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_request_params(agent_type: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
标准化不同Agent的请求参数
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
agent_type: Agent类型(cursor/claude/trea/qoder)
|
|
29
|
+
params: 原始请求参数
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
标准化后的参数
|
|
33
|
+
"""
|
|
34
|
+
agent_type_lower = agent_type.lower()
|
|
35
|
+
mapping = AGENT_FIELD_MAPPINGS.get(agent_type_lower, {})
|
|
36
|
+
|
|
37
|
+
normalized = params.copy()
|
|
38
|
+
|
|
39
|
+
# 字段映射
|
|
40
|
+
for alias, target_field in mapping.items():
|
|
41
|
+
if alias in normalized and alias != target_field:
|
|
42
|
+
if target_field not in normalized:
|
|
43
|
+
normalized[target_field] = normalized[alias]
|
|
44
|
+
del normalized[alias]
|
|
45
|
+
|
|
46
|
+
return normalized
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def detect_agent_type(session_info: Optional[str]) -> str:
|
|
50
|
+
"""
|
|
51
|
+
从session_info中检测Agent类型
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
session_info: 会话信息字符串
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Agent类型(默认返回"unknown")
|
|
58
|
+
"""
|
|
59
|
+
if not session_info:
|
|
60
|
+
return "unknown"
|
|
61
|
+
|
|
62
|
+
session_info_lower = session_info.lower()
|
|
63
|
+
|
|
64
|
+
for agent_type in ["cursor", "claude", "trea", "qoder"]:
|
|
65
|
+
if agent_type in session_info_lower:
|
|
66
|
+
return agent_type
|
|
67
|
+
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
mcp/api_schemas.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""请求/响应 Pydantic 模型"""
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RecordBeforeEditRequest(BaseModel):
|
|
7
|
+
"""RecordBeforeEdit Tool请求模型"""
|
|
8
|
+
session_id: str = Field(..., description="当前会话ID,由Agent生成,需与RecordAfterEdit的session_id一致")
|
|
9
|
+
file_path: str = Field(..., description="目标文件的绝对路径")
|
|
10
|
+
code_before: str = Field(..., description="文件编辑前的完整代码内容")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RecordAfterEditRequest(BaseModel):
|
|
14
|
+
"""RecordAfterEdit Tool请求模型"""
|
|
15
|
+
session_id: str = Field(..., description="当前会话ID,需与RecordBeforeEdit的session_id一致")
|
|
16
|
+
file_path: str = Field(..., description="目标文件的绝对路径,需与RecordBeforeEdit的file_path一致")
|
|
17
|
+
code_after: str = Field(..., description="文件编辑后的完整代码内容")
|
|
18
|
+
session_info: Optional[str] = Field(None, description="会话补充信息(如用户指令、Agent类型、操作时间)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPResponse(BaseModel):
|
|
22
|
+
"""MCP Tool统一响应模型"""
|
|
23
|
+
status: str = Field(..., description="状态:success 或 error")
|
|
24
|
+
message: str = Field(..., description="响应消息")
|
|
25
|
+
data: Optional[dict] = Field(None, description="响应数据(可选)")
|
|
26
|
+
|